@swarmclawai/swarmclaw 1.2.4 → 1.2.5

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 (260) hide show
  1. package/README.md +14 -0
  2. package/bin/daemon-cmd.js +169 -0
  3. package/bin/server-cmd.js +3 -0
  4. package/bin/swarmclaw.js +11 -0
  5. package/package.json +17 -16
  6. package/src/app/api/agents/[id]/clone/route.ts +3 -32
  7. package/src/app/api/agents/[id]/route.ts +6 -158
  8. package/src/app/api/agents/[id]/status/route.ts +2 -3
  9. package/src/app/api/agents/[id]/thread/route.ts +4 -17
  10. package/src/app/api/agents/bulk/route.ts +5 -47
  11. package/src/app/api/agents/route.ts +5 -119
  12. package/src/app/api/agents/trash/route.ts +13 -24
  13. package/src/app/api/auth/route.ts +3 -9
  14. package/src/app/api/autonomy/estop/route.ts +5 -5
  15. package/src/app/api/chatrooms/[id]/chat/route.ts +11 -5
  16. package/src/app/api/chatrooms/[id]/route.ts +23 -2
  17. package/src/app/api/chatrooms/route.ts +13 -2
  18. package/src/app/api/chats/[id]/clear/route.ts +2 -13
  19. package/src/app/api/chats/[id]/deploy/route.ts +2 -3
  20. package/src/app/api/chats/[id]/edit-resend/route.ts +7 -13
  21. package/src/app/api/chats/[id]/mailbox/route.ts +6 -8
  22. package/src/app/api/chats/[id]/queue/route.ts +17 -64
  23. package/src/app/api/chats/[id]/retry/route.ts +4 -22
  24. package/src/app/api/chats/[id]/route.ts +10 -138
  25. package/src/app/api/chats/heartbeat/route.ts +2 -1
  26. package/src/app/api/chats/migrate-messages/route.ts +7 -0
  27. package/src/app/api/chats/route.ts +13 -134
  28. package/src/app/api/connectors/[id]/access/route.ts +12 -229
  29. package/src/app/api/connectors/[id]/doctor/route.ts +1 -1
  30. package/src/app/api/connectors/[id]/health/route.ts +12 -39
  31. package/src/app/api/connectors/[id]/route.ts +14 -122
  32. package/src/app/api/connectors/[id]/webhook/route.ts +1 -1
  33. package/src/app/api/connectors/doctor/route.ts +1 -1
  34. package/src/app/api/connectors/route.ts +12 -70
  35. package/src/app/api/credentials/[id]/route.ts +2 -4
  36. package/src/app/api/credentials/route.ts +10 -19
  37. package/src/app/api/daemon/health-check/route.ts +3 -4
  38. package/src/app/api/daemon/route.ts +10 -8
  39. package/src/app/api/documents/route.ts +11 -10
  40. package/src/app/api/external-agents/route.ts +3 -3
  41. package/src/app/api/gateways/[id]/health/route.ts +2 -3
  42. package/src/app/api/gateways/[id]/route.ts +7 -122
  43. package/src/app/api/gateways/route.ts +3 -103
  44. package/src/app/api/mcp-servers/[id]/tools/route.ts +5 -5
  45. package/src/app/api/openclaw/dashboard-url/route.ts +8 -16
  46. package/src/app/api/openclaw/directory/route.ts +2 -2
  47. package/src/app/api/openclaw/history/route.ts +3 -5
  48. package/src/app/api/providers/[id]/route.test.ts +49 -0
  49. package/src/app/api/providers/ollama/route.ts +6 -5
  50. package/src/app/api/schedules/[id]/route.ts +14 -108
  51. package/src/app/api/schedules/[id]/run/route.ts +6 -67
  52. package/src/app/api/schedules/route.ts +9 -51
  53. package/src/app/api/settings/route.ts +4 -3
  54. package/src/app/api/setup/check-provider/route.ts +15 -1
  55. package/src/app/api/setup/openclaw-device/route.ts +2 -2
  56. package/src/app/api/system/status/route.ts +2 -2
  57. package/src/app/api/tasks/[id]/route.ts +16 -202
  58. package/src/app/api/tasks/bulk/route.ts +5 -86
  59. package/src/app/api/tasks/metrics/route.ts +2 -1
  60. package/src/app/api/tasks/route.ts +11 -171
  61. package/src/app/api/upload/route.ts +1 -1
  62. package/src/app/api/uploads/[filename]/route.ts +1 -1
  63. package/src/app/api/uploads/route.ts +1 -1
  64. package/src/app/api/webhooks/[id]/history/route.ts +2 -2
  65. package/src/app/layout.tsx +9 -6
  66. package/src/app/protocols/page.tsx +71 -89
  67. package/src/app/tasks/page.tsx +32 -32
  68. package/src/cli/index.js +1 -0
  69. package/src/cli/spec.js +1 -0
  70. package/src/components/agents/agent-sheet.tsx +5 -5
  71. package/src/components/auth/setup-wizard/index.tsx +4 -4
  72. package/src/components/auth/setup-wizard/step-agents.tsx +1 -1
  73. package/src/components/auth/setup-wizard/step-connect.tsx +1 -1
  74. package/src/components/auth/setup-wizard/utils.ts +1 -1
  75. package/src/components/chatrooms/chatroom-sheet.tsx +16 -276
  76. package/src/components/connectors/connector-list.tsx +26 -40
  77. package/src/components/connectors/connector-sheet.tsx +95 -149
  78. package/src/components/gateways/gateway-sheet.tsx +61 -110
  79. package/src/components/layout/live-query-sync.tsx +121 -0
  80. package/src/components/protocols/structured-session-launcher.tsx +24 -45
  81. package/src/components/providers/app-query-provider.tsx +17 -0
  82. package/src/components/providers/provider-list.tsx +60 -61
  83. package/src/components/providers/provider-sheet.tsx +74 -56
  84. package/src/components/skills/skill-list.tsx +5 -18
  85. package/src/components/skills/skill-sheet.tsx +21 -20
  86. package/src/components/skills/skills-workspace.tsx +48 -87
  87. package/src/components/tasks/task-card.tsx +20 -13
  88. package/src/components/tasks/task-column.tsx +22 -7
  89. package/src/components/tasks/task-list.tsx +8 -11
  90. package/src/components/tasks/task-sheet.tsx +111 -103
  91. package/src/features/agents/queries.ts +20 -0
  92. package/src/features/chatrooms/queries.ts +20 -0
  93. package/src/features/chats/queries.ts +27 -0
  94. package/src/features/connectors/queries.ts +145 -0
  95. package/src/features/credentials/queries.ts +37 -0
  96. package/src/features/extensions/queries.ts +26 -0
  97. package/src/features/external-agents/queries.ts +36 -0
  98. package/src/features/gateways/queries.ts +274 -0
  99. package/src/features/missions/queries.ts +23 -0
  100. package/src/features/projects/queries.ts +20 -0
  101. package/src/features/protocols/queries.ts +149 -0
  102. package/src/features/providers/queries.ts +142 -0
  103. package/src/features/settings/queries.ts +20 -0
  104. package/src/features/skills/queries.ts +182 -0
  105. package/src/features/tasks/queries.ts +189 -0
  106. package/src/hooks/use-ws.ts +3 -2
  107. package/src/lib/app/api-client.ts +2 -2
  108. package/src/lib/query/client.ts +17 -0
  109. package/src/lib/server/agents/agent-runtime-config.ts +1 -1
  110. package/src/lib/server/agents/agent-service.ts +429 -0
  111. package/src/lib/server/agents/agent-thread-session.ts +6 -5
  112. package/src/lib/server/agents/autonomy-contract.ts +1 -4
  113. package/src/lib/server/agents/delegation-advisory.test.ts +206 -0
  114. package/src/lib/server/agents/delegation-advisory.ts +251 -0
  115. package/src/lib/server/agents/main-agent-loop.ts +98 -40
  116. package/src/lib/server/agents/subagent-runtime.ts +12 -0
  117. package/src/lib/server/autonomy/supervisor-reflection.test.ts +20 -1
  118. package/src/lib/server/autonomy/supervisor-reflection.ts +39 -19
  119. package/src/lib/server/build-llm.ts +7 -15
  120. package/src/lib/server/capability-router.test.ts +70 -1
  121. package/src/lib/server/capability-router.ts +24 -99
  122. package/src/lib/server/chat-execution/chat-execution-utils.ts +0 -15
  123. package/src/lib/server/chat-execution/chat-streaming-utils.ts +2 -4
  124. package/src/lib/server/chat-execution/chat-turn-finalization.ts +77 -12
  125. package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +4 -4
  126. package/src/lib/server/chat-execution/chat-turn-preflight.ts +2 -2
  127. package/src/lib/server/chat-execution/chat-turn-preparation.ts +41 -17
  128. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +4 -2
  129. package/src/lib/server/chat-execution/chat-turn-tool-routing.test.ts +45 -0
  130. package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +48 -17
  131. package/src/lib/server/chat-execution/continuation-evaluator.ts +4 -1
  132. package/src/lib/server/chat-execution/direct-memory-intent.test.ts +9 -0
  133. package/src/lib/server/chat-execution/direct-memory-intent.ts +12 -2
  134. package/src/lib/server/chat-execution/message-classifier.test.ts +35 -23
  135. package/src/lib/server/chat-execution/message-classifier.ts +74 -32
  136. package/src/lib/server/chat-execution/prompt-builder.test.ts +29 -0
  137. package/src/lib/server/chat-execution/prompt-builder.ts +37 -2
  138. package/src/lib/server/chat-execution/prompt-sections.test.ts +56 -0
  139. package/src/lib/server/chat-execution/prompt-sections.ts +193 -0
  140. package/src/lib/server/chat-execution/stream-agent-chat.ts +63 -7
  141. package/src/lib/server/chat-execution/stream-continuation.test.ts +36 -0
  142. package/src/lib/server/chat-execution/stream-continuation.ts +28 -13
  143. package/src/lib/server/chatrooms/chatroom-agent-signals.ts +26 -18
  144. package/src/lib/server/chatrooms/chatroom-helpers.ts +19 -18
  145. package/src/lib/server/chatrooms/chatroom-repository.ts +16 -0
  146. package/src/lib/server/chatrooms/chatroom-routing.test.ts +96 -0
  147. package/src/lib/server/chatrooms/chatroom-routing.ts +207 -53
  148. package/src/lib/server/chatrooms/mailbox-utils.ts +4 -2
  149. package/src/lib/server/chatrooms/session-mailbox.ts +50 -40
  150. package/src/lib/server/chats/chat-session-service.ts +410 -0
  151. package/src/lib/server/connectors/access.ts +1 -1
  152. package/src/lib/server/connectors/commands.ts +7 -6
  153. package/src/lib/server/connectors/connector-inbound.ts +14 -7
  154. package/src/lib/server/connectors/connector-outbound.ts +16 -11
  155. package/src/lib/server/connectors/connector-service.ts +453 -0
  156. package/src/lib/server/connectors/delivery.ts +17 -12
  157. package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -14
  158. package/src/lib/server/connectors/media.ts +1 -1
  159. package/src/lib/server/connectors/response-media.ts +1 -1
  160. package/src/lib/server/connectors/session-consolidation.ts +11 -7
  161. package/src/lib/server/connectors/session.ts +9 -7
  162. package/src/lib/server/connectors/voice-note.ts +2 -1
  163. package/src/lib/server/context-manager.ts +20 -1
  164. package/src/lib/server/cost.ts +2 -3
  165. package/src/lib/server/credentials/credential-repository.ts +43 -4
  166. package/src/lib/server/credentials/credential-service.ts +112 -0
  167. package/src/lib/server/daemon/admin-metadata.ts +64 -0
  168. package/src/lib/server/daemon/controller.ts +577 -0
  169. package/src/lib/server/daemon/daemon-runtime.ts +352 -0
  170. package/src/lib/server/daemon/daemon-status-repository.ts +63 -0
  171. package/src/lib/server/daemon/types.ts +101 -0
  172. package/src/lib/server/embeddings.ts +3 -9
  173. package/src/lib/server/eval/agent-regression.ts +3 -2
  174. package/src/lib/server/eval/runner.ts +2 -2
  175. package/src/lib/server/execution-brief.test.ts +167 -0
  176. package/src/lib/server/execution-brief.ts +295 -0
  177. package/src/lib/server/execution-engine/chat-turn.ts +9 -0
  178. package/src/lib/server/execution-engine/import-boundary.test.ts +44 -0
  179. package/src/lib/server/execution-engine/index.ts +35 -0
  180. package/src/lib/server/execution-engine/task-attempt.ts +303 -0
  181. package/src/lib/server/execution-engine/types.ts +33 -0
  182. package/src/lib/server/gateways/gateway-profile-repository.ts +47 -3
  183. package/src/lib/server/gateways/gateway-profile-service.ts +200 -0
  184. package/src/lib/server/memory/session-archive-memory.ts +12 -10
  185. package/src/lib/server/messages/message-repository.ts +330 -0
  186. package/src/lib/server/missions/mission-service/core.ts +8 -6
  187. package/src/lib/server/openclaw/agent-resolver.ts +2 -3
  188. package/src/lib/server/openclaw/doctor.ts +1 -1
  189. package/src/lib/server/openclaw/gateway.test.ts +10 -1
  190. package/src/lib/server/openclaw/gateway.ts +5 -14
  191. package/src/lib/server/openclaw/health.ts +3 -11
  192. package/src/lib/server/openclaw/sync.ts +8 -6
  193. package/src/lib/server/persistence/storage-context.ts +3 -0
  194. package/src/lib/server/protocols/protocol-agent-turn.ts +25 -17
  195. package/src/lib/server/protocols/protocol-normalization.ts +1 -1
  196. package/src/lib/server/protocols/protocol-queries.ts +13 -7
  197. package/src/lib/server/protocols/protocol-run-lifecycle.ts +16 -20
  198. package/src/lib/server/protocols/protocol-run-repository.ts +81 -0
  199. package/src/lib/server/protocols/protocol-step-processors.ts +23 -31
  200. package/src/lib/server/protocols/protocol-swarm.ts +8 -8
  201. package/src/lib/server/protocols/protocol-template-repository.ts +42 -0
  202. package/src/lib/server/protocols/protocol-templates.ts +4 -2
  203. package/src/lib/server/protocols/protocol-types.ts +10 -7
  204. package/src/lib/server/provider-endpoint.ts +7 -12
  205. package/src/lib/server/provider-model-discovery.ts +2 -11
  206. package/src/lib/server/query-expansion.ts +5 -6
  207. package/src/lib/server/run-context.test.ts +365 -0
  208. package/src/lib/server/run-context.ts +367 -0
  209. package/src/lib/server/runtime/heartbeat-service.ts +7 -5
  210. package/src/lib/server/runtime/queue/core.ts +61 -190
  211. package/src/lib/server/runtime/run-ledger.ts +8 -0
  212. package/src/lib/server/runtime/session-run-manager/drain.ts +2 -2
  213. package/src/lib/server/runtime/session-run-manager/enqueue.ts +6 -0
  214. package/src/lib/server/runtime/session-run-manager/state.ts +4 -0
  215. package/src/lib/server/schedules/schedule-route-service.ts +230 -0
  216. package/src/lib/server/service-result.ts +16 -0
  217. package/src/lib/server/session-note.ts +2 -3
  218. package/src/lib/server/session-reset-policy.ts +4 -3
  219. package/src/lib/server/session-tools/connector.ts +9 -6
  220. package/src/lib/server/session-tools/context-mgmt.ts +58 -9
  221. package/src/lib/server/session-tools/crud.ts +162 -10
  222. package/src/lib/server/session-tools/delegate.ts +1 -1
  223. package/src/lib/server/session-tools/manage-tasks.test.ts +152 -0
  224. package/src/lib/server/session-tools/memory.ts +6 -4
  225. package/src/lib/server/session-tools/session-info.test.ts +56 -0
  226. package/src/lib/server/session-tools/session-info.ts +119 -12
  227. package/src/lib/server/session-tools/skill-runtime.ts +3 -1
  228. package/src/lib/server/session-tools/skills.ts +15 -15
  229. package/src/lib/server/session-tools/subagent.test.ts +115 -1
  230. package/src/lib/server/session-tools/subagent.ts +125 -7
  231. package/src/lib/server/session-tools/team-context.ts +4 -3
  232. package/src/lib/server/session-tools/wallet.ts +0 -58
  233. package/src/lib/server/sessions/session-lineage.ts +55 -0
  234. package/src/lib/server/sessions/session-repository.ts +2 -2
  235. package/src/lib/server/skills/learned-skills.ts +24 -23
  236. package/src/lib/server/skills/runtime-skill-resolver.ts +2 -1
  237. package/src/lib/server/skills/skill-repository.ts +136 -13
  238. package/src/lib/server/skills/skill-suggestions.ts +25 -28
  239. package/src/lib/server/storage-normalization.test.ts +44 -267
  240. package/src/lib/server/storage-normalization.ts +75 -0
  241. package/src/lib/server/storage.ts +19 -0
  242. package/src/lib/server/structured-extract.ts +3 -14
  243. package/src/lib/server/tasks/task-followups.ts +16 -11
  244. package/src/lib/server/tasks/task-result.test.ts +25 -29
  245. package/src/lib/server/tasks/task-result.ts +5 -9
  246. package/src/lib/server/tasks/task-route-service.ts +449 -0
  247. package/src/lib/server/text-normalization.ts +41 -0
  248. package/src/lib/server/tool-planning.ts +6 -42
  249. package/src/lib/server/upload-path.ts +5 -0
  250. package/src/lib/server/working-state/extraction.ts +614 -0
  251. package/src/lib/server/working-state/normalization.ts +866 -0
  252. package/src/lib/server/working-state/prompt.ts +60 -0
  253. package/src/lib/server/working-state/repository.ts +38 -0
  254. package/src/lib/server/working-state/service.test.ts +253 -0
  255. package/src/lib/server/working-state/service.ts +293 -0
  256. package/src/lib/validation/schemas.ts +1 -0
  257. package/src/lib/ws-client.ts +3 -3
  258. package/src/stores/slices/task-slice.ts +1 -4
  259. package/src/stores/use-chatroom-store.ts +2 -2
  260. package/src/types/index.ts +277 -12
@@ -0,0 +1,303 @@
1
+ import { checkAgentBudgetLimits } from '@/lib/server/cost'
2
+ import { WORKSPACE_DIR } from '@/lib/server/data-dir'
3
+ import { log } from '@/lib/server/logger'
4
+ import { loadSettings } from '@/lib/server/settings/settings-repository'
5
+ import { loadSessions } from '@/lib/server/sessions/session-repository'
6
+ import { appendPersistedRunEvent, persistRun } from '@/lib/server/runtime/run-ledger'
7
+ import { notify } from '@/lib/server/ws-hub'
8
+ import { captureGuardianCheckpoint } from '@/lib/server/agents/guardian'
9
+ import {
10
+ assessAutonomyRun,
11
+ executeSupervisorAutoActions,
12
+ } from '@/lib/server/autonomy/supervisor-reflection'
13
+ import { errorMessage, hmrSingleton } from '@/lib/shared-utils'
14
+ import type {
15
+ Agent,
16
+ BoardTask,
17
+ Session,
18
+ SessionRunRecord,
19
+ SessionRunStatus,
20
+ } from '@/types'
21
+ import type { ExecuteChatTurnResult } from '@/lib/server/chat-execution/chat-execution-types'
22
+ import type {
23
+ EnqueueTaskAttemptExecutionRequest,
24
+ ExecutionHandle,
25
+ } from '@/lib/server/execution-engine/types'
26
+ import { executeExecutionChatTurn } from '@/lib/server/execution-engine/chat-turn'
27
+
28
+ const TAG = 'execution-engine'
29
+
30
+ interface TaskAttemptState {
31
+ runningByTaskId: Map<string, ExecutionHandle<ExecuteChatTurnResult>>
32
+ }
33
+
34
+ const taskAttemptState = hmrSingleton<TaskAttemptState>(
35
+ '__swarmclaw_execution_engine_task_attempt__',
36
+ () => ({
37
+ runningByTaskId: new Map<string, ExecutionHandle<ExecuteChatTurnResult>>(),
38
+ }),
39
+ )
40
+
41
+ function messagePreview(text: string): string {
42
+ return (text || '').replace(/\s+/g, ' ').trim().slice(0, 140)
43
+ }
44
+
45
+ function looksIncomplete(text: string): boolean {
46
+ if (!text) return false
47
+ const trimmed = text.trim()
48
+ if (trimmed.endsWith('...') || trimmed.endsWith('…')) return true
49
+ if (/(?:^|\n)#{1,3}\s+(?:Step|Phase|Next)\s+\d/i.test(trimmed.slice(-200))) return true
50
+ const lastChunk = trimmed.slice(-300).toLowerCase()
51
+ return /\b(?:next i(?:'ll| will)|now i(?:'ll| will)|let me (?:now|next)|moving on to|proceeding to)\b/.test(lastChunk)
52
+ }
53
+
54
+ function chainCallerSignal(callerSignal: AbortSignal | undefined, controller: AbortController): void {
55
+ if (!callerSignal) return
56
+ if (callerSignal.aborted) {
57
+ controller.abort()
58
+ return
59
+ }
60
+ const onAbort = () => controller.abort()
61
+ callerSignal.addEventListener('abort', onAbort, { once: true })
62
+ }
63
+
64
+ function notifyExecutionState(sessionId: string): void {
65
+ notify('runs')
66
+ notify('sessions')
67
+ notify(`session:${sessionId}`)
68
+ }
69
+
70
+ function emitStatus(run: SessionRunRecord, status: SessionRunStatus, extra?: Record<string, unknown>): void {
71
+ appendPersistedRunEvent({
72
+ runId: run.id,
73
+ sessionId: run.sessionId,
74
+ kind: run.kind,
75
+ ownerType: run.ownerType,
76
+ ownerId: run.ownerId,
77
+ parentExecutionId: run.parentExecutionId,
78
+ phase: 'status',
79
+ status,
80
+ summary: run.resultPreview || run.error || undefined,
81
+ event: {
82
+ t: 'md',
83
+ text: JSON.stringify({
84
+ run: {
85
+ id: run.id,
86
+ sessionId: run.sessionId,
87
+ kind: run.kind,
88
+ ownerType: run.ownerType,
89
+ ownerId: run.ownerId,
90
+ status,
91
+ source: run.source,
92
+ internal: run.internal,
93
+ ...extra,
94
+ },
95
+ }),
96
+ },
97
+ })
98
+ notifyExecutionState(run.sessionId)
99
+ }
100
+
101
+ async function executeTaskAttemptTurn(
102
+ task: BoardTask,
103
+ agent: Agent,
104
+ sessionId: string,
105
+ signal: AbortSignal,
106
+ ): Promise<ExecuteChatTurnResult> {
107
+ if (agent.autoRecovery) {
108
+ const cwd = task.projectId
109
+ ? `${WORKSPACE_DIR}/projects/${task.projectId}`
110
+ : WORKSPACE_DIR
111
+ captureGuardianCheckpoint(cwd, `task:${task.id}`)
112
+ }
113
+
114
+ const settings = loadSettings()
115
+ const basePrompt = task.description || task.title
116
+ const prompt = [
117
+ basePrompt,
118
+ '',
119
+ 'Completion requirements:',
120
+ '- Execute the task before replying; do not reply with only a plan.',
121
+ '- Include concrete evidence in your final summary: changed file paths, commands run, and verification results.',
122
+ '- If blocked, state the blocker explicitly and what input or permission is missing.',
123
+ ].join('\n')
124
+
125
+ let latestRun = await executeExecutionChatTurn({
126
+ sessionId,
127
+ message: prompt,
128
+ internal: false,
129
+ source: 'task',
130
+ runId: task.id,
131
+ signal,
132
+ })
133
+ let text = typeof latestRun.text === 'string' ? latestRun.text.trim() : ''
134
+ let previousSummary: string | null = null
135
+ let totalInputTokens = latestRun.inputTokens || 0
136
+ let totalOutputTokens = latestRun.outputTokens || 0
137
+ let totalEstimatedCost = Number(latestRun.estimatedCost || 0)
138
+
139
+ if (latestRun.error) {
140
+ return {
141
+ ...latestRun,
142
+ text,
143
+ }
144
+ }
145
+
146
+ const maxSupervisorFollowups = 2
147
+ for (let followupIndex = 0; followupIndex < maxSupervisorFollowups; followupIndex += 1) {
148
+ if (signal.aborted) break
149
+
150
+ const sessions = loadSessions()
151
+ const session = sessions[sessionId] as Session | undefined
152
+ const assessment = assessAutonomyRun({
153
+ runId: `${task.id}:attempt-${(task.attempts || 0) + 1}:step-${followupIndex + 1}`,
154
+ sessionId,
155
+ taskId: task.id,
156
+ agentId: agent.id,
157
+ source: 'task',
158
+ status: latestRun.error ? 'failed' : 'completed',
159
+ resultText: text,
160
+ error: latestRun.error,
161
+ toolEvents: latestRun.toolEvents,
162
+ mainLoopState: {
163
+ followupChainCount: followupIndex + 1,
164
+ summary: previousSummary,
165
+ missionCostUsd: totalEstimatedCost,
166
+ },
167
+ session: session || null,
168
+ settings,
169
+ })
170
+ if (assessment.shouldBlock) break
171
+ if (assessment.autoActions?.length) {
172
+ const result = await executeSupervisorAutoActions({
173
+ actions: assessment.autoActions,
174
+ sessionId,
175
+ agentId: agent.id,
176
+ })
177
+ if (result.blocked) break
178
+ }
179
+ const followupMessage = assessment.interventionPrompt
180
+ || (text && looksIncomplete(text)
181
+ ? 'Continue and complete the remaining steps. Provide a final summary when done.'
182
+ : null)
183
+ if (!followupMessage) break
184
+
185
+ if (agent.monthlyBudget || agent.dailyBudget || agent.hourlyBudget) {
186
+ try {
187
+ const followupBudget = checkAgentBudgetLimits(agent)
188
+ if (!followupBudget.ok) {
189
+ log.warn(TAG, `[task_attempt] Budget exceeded for "${agent.name}" during follow-up, stopping.`)
190
+ break
191
+ }
192
+ } catch {
193
+ // Best-effort safety check only.
194
+ }
195
+ }
196
+
197
+ previousSummary = text || previousSummary
198
+ const followUp = await executeExecutionChatTurn({
199
+ sessionId,
200
+ message: followupMessage,
201
+ internal: false,
202
+ source: 'task',
203
+ signal,
204
+ })
205
+ totalInputTokens += followUp.inputTokens || 0
206
+ totalOutputTokens += followUp.outputTokens || 0
207
+ totalEstimatedCost += Number(followUp.estimatedCost || 0)
208
+ text = typeof followUp.text === 'string' ? followUp.text.trim() : ''
209
+ latestRun = {
210
+ ...followUp,
211
+ text,
212
+ inputTokens: totalInputTokens,
213
+ outputTokens: totalOutputTokens,
214
+ estimatedCost: totalEstimatedCost,
215
+ }
216
+ if (latestRun.error) break
217
+ }
218
+
219
+ return {
220
+ ...latestRun,
221
+ text,
222
+ inputTokens: totalInputTokens,
223
+ outputTokens: totalOutputTokens,
224
+ estimatedCost: totalEstimatedCost,
225
+ }
226
+ }
227
+
228
+ export function enqueueTaskAttemptExecution(
229
+ input: EnqueueTaskAttemptExecutionRequest,
230
+ ): ExecutionHandle<ExecuteChatTurnResult> {
231
+ const existing = taskAttemptState.runningByTaskId.get(input.task.id)
232
+ if (existing) return { ...existing, deduped: true }
233
+
234
+ const executionId = input.executionId || `${input.task.id}:attempt-${(input.task.attempts || 0) + 1}`
235
+ const controller = new AbortController()
236
+ chainCallerSignal(input.callerSignal, controller)
237
+
238
+ const run: SessionRunRecord = {
239
+ id: executionId,
240
+ sessionId: input.sessionId,
241
+ missionId: input.task.missionId || null,
242
+ kind: 'task_attempt',
243
+ ownerType: 'task',
244
+ ownerId: input.task.id,
245
+ parentExecutionId: null,
246
+ recoveryPolicy: 'restart_recoverable',
247
+ source: 'task',
248
+ internal: false,
249
+ mode: 'task_attempt',
250
+ status: 'queued',
251
+ messagePreview: messagePreview(input.task.description || input.task.title),
252
+ queuedAt: Date.now(),
253
+ }
254
+
255
+ persistRun(run)
256
+ emitStatus(run, 'queued')
257
+
258
+ const promise = (async () => {
259
+ run.status = 'running'
260
+ run.startedAt = Date.now()
261
+ persistRun(run)
262
+ emitStatus(run, 'running')
263
+
264
+ try {
265
+ const result = await executeTaskAttemptTurn(input.task, input.agent, input.sessionId, controller.signal)
266
+ run.status = controller.signal.aborted
267
+ ? 'cancelled'
268
+ : (result.error ? 'failed' : 'completed')
269
+ run.endedAt = Date.now()
270
+ run.error = controller.signal.aborted ? (run.error || 'Cancelled') : result.error
271
+ run.resultPreview = result.text?.slice(0, 280)
272
+ if (typeof result.inputTokens === 'number') run.totalInputTokens = result.inputTokens
273
+ if (typeof result.outputTokens === 'number') run.totalOutputTokens = result.outputTokens
274
+ if (typeof result.estimatedCost === 'number') run.estimatedCost = result.estimatedCost
275
+ persistRun(run)
276
+ emitStatus(run, run.status, {
277
+ hasText: !!result.text,
278
+ error: run.error || null,
279
+ })
280
+ return result
281
+ } catch (err: unknown) {
282
+ run.status = controller.signal.aborted ? 'cancelled' : 'failed'
283
+ run.endedAt = Date.now()
284
+ run.error = errorMessage(err)
285
+ persistRun(run)
286
+ emitStatus(run, run.status, { error: run.error })
287
+ throw err
288
+ } finally {
289
+ const latest = taskAttemptState.runningByTaskId.get(input.task.id)
290
+ if (latest?.executionId === executionId) {
291
+ taskAttemptState.runningByTaskId.delete(input.task.id)
292
+ }
293
+ }
294
+ })()
295
+
296
+ const handle: ExecutionHandle<ExecuteChatTurnResult> = {
297
+ executionId,
298
+ promise,
299
+ abort: () => controller.abort(),
300
+ }
301
+ taskAttemptState.runningByTaskId.set(input.task.id, handle)
302
+ return handle
303
+ }
@@ -0,0 +1,33 @@
1
+ import type { ExecuteChatTurnResult } from '@/lib/server/chat-execution/chat-execution-types'
2
+ import type { EnqueueSessionRunInput } from '@/lib/server/runtime/session-run-manager/types'
3
+ import type { Agent, BoardTask } from '@/types'
4
+
5
+ export interface ExecutionHandle<TResult> {
6
+ executionId: string
7
+ promise: Promise<TResult>
8
+ abort: () => void
9
+ position?: number
10
+ deduped?: boolean
11
+ coalesced?: boolean
12
+ unsubscribe?: () => void
13
+ }
14
+
15
+ export interface EnqueueSessionTurnExecutionRequest {
16
+ kind: 'session_turn'
17
+ input: EnqueueSessionRunInput
18
+ }
19
+
20
+ export interface EnqueueTaskAttemptExecutionRequest {
21
+ kind: 'task_attempt'
22
+ task: BoardTask
23
+ agent: Agent
24
+ sessionId: string
25
+ executionId?: string
26
+ callerSignal?: AbortSignal
27
+ }
28
+
29
+ export type EnqueueExecutionRequest =
30
+ | EnqueueSessionTurnExecutionRequest
31
+ | EnqueueTaskAttemptExecutionRequest
32
+
33
+ export type ExecutionResult = ExecuteChatTurnResult
@@ -1,4 +1,48 @@
1
- export {
2
- loadGatewayProfiles,
3
- saveGatewayProfiles,
1
+ import type { GatewayProfile } from '@/types'
2
+
3
+ import {
4
+ deleteStoredItem,
5
+ loadGatewayProfiles as loadStoredGatewayProfiles,
6
+ loadStoredItem,
7
+ saveGatewayProfiles as saveStoredGatewayProfiles,
8
+ upsertStoredItem,
4
9
  } from '@/lib/server/storage'
10
+ import { createRecordRepository } from '@/lib/server/persistence/repository-utils'
11
+
12
+ export const gatewayProfileRepository = createRecordRepository<GatewayProfile>(
13
+ 'gatewayProfiles',
14
+ {
15
+ get(id) {
16
+ return loadStoredItem('gateway_profiles', id) as GatewayProfile | null
17
+ },
18
+ list() {
19
+ return loadStoredGatewayProfiles() as Record<string, GatewayProfile>
20
+ },
21
+ upsert(id, value) {
22
+ upsertStoredItem('gateway_profiles', id, value)
23
+ },
24
+ replace(data) {
25
+ saveStoredGatewayProfiles(data as Record<string, GatewayProfile>)
26
+ },
27
+ patch(id, updater) {
28
+ const current = loadStoredItem('gateway_profiles', id) as GatewayProfile | null
29
+ const next = updater(current)
30
+ if (next === null) {
31
+ deleteStoredItem('gateway_profiles', id)
32
+ return null
33
+ }
34
+ upsertStoredItem('gateway_profiles', id, next)
35
+ return next
36
+ },
37
+ delete(id) {
38
+ deleteStoredItem('gateway_profiles', id)
39
+ },
40
+ },
41
+ )
42
+
43
+ export const loadGatewayProfiles = () => gatewayProfileRepository.list()
44
+ export const loadGatewayProfile = (id: string) => gatewayProfileRepository.get(id)
45
+ export const saveGatewayProfiles = (items: Record<string, GatewayProfile | Record<string, unknown>>) => gatewayProfileRepository.replace(items as Record<string, GatewayProfile>)
46
+ export const saveGatewayProfile = (id: string, value: GatewayProfile | Record<string, unknown>) => gatewayProfileRepository.upsert(id, value as GatewayProfile)
47
+ export const patchGatewayProfile = (id: string, updater: (current: GatewayProfile | null) => GatewayProfile | null) => gatewayProfileRepository.patch(id, updater)
48
+ export const deleteGatewayProfile = (id: string) => gatewayProfileRepository.delete(id)
@@ -0,0 +1,200 @@
1
+ import type { Agent, AgentRoutingTarget, GatewayProfile, OpenClawDeploymentConfig, OpenClawGatewayStats } from '@/types'
2
+
3
+ import { genId } from '@/lib/id'
4
+ import { normalizeOpenClawEndpoint } from '@/lib/openclaw/openclaw-endpoint'
5
+ import { listAgents, saveAgentMany } from '@/lib/server/agents/agent-repository'
6
+ import { getGatewayProfiles } from '@/lib/server/agents/agent-runtime-config'
7
+ import {
8
+ loadGatewayProfile,
9
+ loadGatewayProfiles,
10
+ saveGatewayProfiles,
11
+ } from '@/lib/server/gateways/gateway-profile-repository'
12
+ import { notify } from '@/lib/server/ws-hub'
13
+
14
+ function normalizeTags(value: unknown): string[] {
15
+ if (!Array.isArray(value)) return []
16
+ return value
17
+ .map((entry) => (typeof entry === 'string' ? entry.trim() : ''))
18
+ .filter(Boolean)
19
+ }
20
+
21
+ function normalizeText(value: unknown): string | null {
22
+ return typeof value === 'string' && value.trim() ? value.trim() : null
23
+ }
24
+
25
+ function normalizeNullableNumber(value: unknown): number | null {
26
+ return typeof value === 'number' && Number.isFinite(value) ? value : null
27
+ }
28
+
29
+ function normalizeDeployment(value: unknown): OpenClawDeploymentConfig | null {
30
+ if (!value || typeof value !== 'object') return null
31
+ const deployment = value as Record<string, unknown>
32
+ return {
33
+ method: normalizeText(deployment.method) as OpenClawDeploymentConfig['method'],
34
+ provider: normalizeText(deployment.provider) as OpenClawDeploymentConfig['provider'],
35
+ remoteTarget: normalizeText(deployment.remoteTarget) as OpenClawDeploymentConfig['remoteTarget'],
36
+ useCase: normalizeText(deployment.useCase) as OpenClawDeploymentConfig['useCase'],
37
+ exposure: normalizeText(deployment.exposure) as OpenClawDeploymentConfig['exposure'],
38
+ managedBy: normalizeText(deployment.managedBy) as OpenClawDeploymentConfig['managedBy'],
39
+ localInstanceId: normalizeText(deployment.localInstanceId),
40
+ localPort: normalizeNullableNumber(deployment.localPort),
41
+ targetHost: normalizeText(deployment.targetHost),
42
+ sshHost: normalizeText(deployment.sshHost),
43
+ sshUser: normalizeText(deployment.sshUser),
44
+ sshPort: normalizeNullableNumber(deployment.sshPort),
45
+ sshKeyPath: normalizeText(deployment.sshKeyPath),
46
+ sshTargetDir: normalizeText(deployment.sshTargetDir),
47
+ image: normalizeText(deployment.image),
48
+ version: normalizeText(deployment.version),
49
+ lastDeployAt: normalizeNullableNumber(deployment.lastDeployAt),
50
+ lastDeployAction: normalizeText(deployment.lastDeployAction),
51
+ lastDeployProcessId: normalizeText(deployment.lastDeployProcessId),
52
+ lastDeploySummary: normalizeText(deployment.lastDeploySummary),
53
+ lastVerifiedAt: normalizeNullableNumber(deployment.lastVerifiedAt),
54
+ lastVerifiedOk: typeof deployment.lastVerifiedOk === 'boolean' ? deployment.lastVerifiedOk : null,
55
+ lastVerifiedMessage: normalizeText(deployment.lastVerifiedMessage),
56
+ lastBackupPath: normalizeText(deployment.lastBackupPath),
57
+ }
58
+ }
59
+
60
+ function normalizeStats(value: unknown): OpenClawGatewayStats | null {
61
+ if (!value || typeof value !== 'object') return null
62
+ const stats = value as Record<string, unknown>
63
+ return {
64
+ nodeCount: normalizeNullableNumber(stats.nodeCount) ?? undefined,
65
+ connectedNodeCount: normalizeNullableNumber(stats.connectedNodeCount) ?? undefined,
66
+ pendingNodePairings: normalizeNullableNumber(stats.pendingNodePairings) ?? undefined,
67
+ pairedDeviceCount: normalizeNullableNumber(stats.pairedDeviceCount) ?? undefined,
68
+ pendingDevicePairings: normalizeNullableNumber(stats.pendingDevicePairings) ?? undefined,
69
+ externalRuntimeCount: normalizeNullableNumber(stats.externalRuntimeCount) ?? undefined,
70
+ }
71
+ }
72
+
73
+ export function listOpenClawGatewayProfiles(): GatewayProfile[] {
74
+ return getGatewayProfiles('openclaw')
75
+ }
76
+
77
+ export function getGatewayProfileById(id: string): GatewayProfile | null {
78
+ return loadGatewayProfile(id)
79
+ }
80
+
81
+ export function createGatewayProfile(input: Record<string, unknown>): GatewayProfile {
82
+ const now = Date.now()
83
+ const gateways = loadGatewayProfiles()
84
+ const id = typeof input.id === 'string' && input.id.trim() ? input.id : `gateway-${genId()}`
85
+ const isDefault = input.isDefault === true
86
+
87
+ if (isDefault) {
88
+ for (const gateway of Object.values(gateways)) {
89
+ if (gateway) gateway.isDefault = false
90
+ }
91
+ }
92
+
93
+ const profile: GatewayProfile = {
94
+ id,
95
+ name: typeof input.name === 'string' && input.name.trim() ? input.name.trim() : 'OpenClaw Gateway',
96
+ provider: 'openclaw',
97
+ endpoint: normalizeOpenClawEndpoint(typeof input.endpoint === 'string' ? input.endpoint : undefined),
98
+ wsUrl: normalizeText(input.wsUrl),
99
+ credentialId: normalizeText(input.credentialId),
100
+ status: typeof input.status === 'string' && input.status.trim() ? input.status as GatewayProfile['status'] : 'unknown',
101
+ notes: typeof input.notes === 'string' ? input.notes : null,
102
+ tags: normalizeTags(input.tags),
103
+ lastError: null,
104
+ lastCheckedAt: null,
105
+ lastModelCount: null,
106
+ discoveredHost: typeof input.discoveredHost === 'string' ? input.discoveredHost : null,
107
+ discoveredPort: typeof input.discoveredPort === 'number' ? input.discoveredPort : null,
108
+ deployment: normalizeDeployment(input.deployment),
109
+ stats: normalizeStats(input.stats),
110
+ isDefault,
111
+ createdAt: now,
112
+ updatedAt: now,
113
+ }
114
+
115
+ gateways[id] = profile
116
+ saveGatewayProfiles(gateways)
117
+ notify('gateways')
118
+ return profile
119
+ }
120
+
121
+ export function updateGatewayProfile(id: string, input: Record<string, unknown>): GatewayProfile | null {
122
+ const gateways = loadGatewayProfiles()
123
+ const gateway = gateways[id]
124
+ if (!gateway) return null
125
+
126
+ // Clear isDefault on other gateways if this one is becoming default
127
+ if (input.isDefault === true) {
128
+ for (const [candidateId, g] of Object.entries(gateways)) {
129
+ if (candidateId !== id && g) g.isDefault = false
130
+ }
131
+ }
132
+
133
+ // Apply all field updates to the target gateway
134
+ if (input.name !== undefined) gateway.name = String(input.name || '').trim() || gateway.name
135
+ if (input.endpoint !== undefined) gateway.endpoint = normalizeOpenClawEndpoint(typeof input.endpoint === 'string' ? input.endpoint : undefined)
136
+ if (input.wsUrl !== undefined) gateway.wsUrl = normalizeText(input.wsUrl)
137
+ if (input.credentialId !== undefined) gateway.credentialId = normalizeText(input.credentialId)
138
+ if (input.status !== undefined) {
139
+ const nextStatus = typeof input.status === 'string' && input.status.trim()
140
+ ? input.status.trim() as GatewayProfile['status']
141
+ : 'unknown'
142
+ gateway.status = nextStatus
143
+ }
144
+ if (input.notes !== undefined) gateway.notes = typeof input.notes === 'string' ? input.notes : null
145
+ if (input.tags !== undefined) gateway.tags = normalizeTags(input.tags)
146
+ if (input.lastError !== undefined) gateway.lastError = typeof input.lastError === 'string' ? input.lastError : null
147
+ if (input.lastCheckedAt !== undefined) gateway.lastCheckedAt = normalizeNullableNumber(input.lastCheckedAt)
148
+ if (input.lastModelCount !== undefined) gateway.lastModelCount = normalizeNullableNumber(input.lastModelCount)
149
+ if (input.discoveredHost !== undefined) gateway.discoveredHost = typeof input.discoveredHost === 'string' ? input.discoveredHost : null
150
+ if (input.discoveredPort !== undefined) gateway.discoveredPort = normalizeNullableNumber(input.discoveredPort)
151
+ if (input.deployment !== undefined) gateway.deployment = { ...(gateway.deployment || {}), ...(normalizeDeployment(input.deployment) || {}) }
152
+ if (input.stats !== undefined) gateway.stats = { ...(gateway.stats || {}), ...(normalizeStats(input.stats) || {}) }
153
+ if (input.isDefault !== undefined) gateway.isDefault = input.isDefault === true
154
+ gateway.updatedAt = Date.now()
155
+
156
+ gateways[id] = gateway
157
+ saveGatewayProfiles(gateways)
158
+ notify('gateways')
159
+ return gateway
160
+ }
161
+
162
+ export function deleteGatewayProfileAndDetachAgents(id: string): boolean {
163
+ const gateways = loadGatewayProfiles()
164
+ if (!gateways[id]) return false
165
+ delete gateways[id]
166
+ saveGatewayProfiles(gateways)
167
+
168
+ const agents = listAgents({ includeTrashed: true })
169
+ const changed: Array<[string, Agent]> = []
170
+ for (const agent of Object.values(agents)) {
171
+ let nextAgent: Agent | null = null
172
+
173
+ if (agent.gatewayProfileId === id) {
174
+ nextAgent = {
175
+ ...agent,
176
+ gatewayProfileId: null,
177
+ }
178
+ }
179
+
180
+ if (Array.isArray(agent.routingTargets)) {
181
+ const nextTargets = agent.routingTargets.map((target: AgentRoutingTarget) => (
182
+ target.gatewayProfileId === id
183
+ ? { ...target, gatewayProfileId: null }
184
+ : target
185
+ ))
186
+ if (JSON.stringify(nextTargets) !== JSON.stringify(agent.routingTargets)) {
187
+ nextAgent = {
188
+ ...(nextAgent || agent),
189
+ routingTargets: nextTargets,
190
+ }
191
+ }
192
+ }
193
+
194
+ if (nextAgent) changed.push([nextAgent.id, nextAgent])
195
+ }
196
+
197
+ if (changed.length > 0) saveAgentMany(changed)
198
+ notify('gateways')
199
+ return true
200
+ }
@@ -1,11 +1,12 @@
1
1
  import { createHash } from 'crypto'
2
2
  import fs from 'fs'
3
3
  import path from 'path'
4
- import type { Agent, MemoryEntry, MemoryReference, Session } from '@/types'
4
+ import type { Agent, MemoryEntry, MemoryReference, Message, Session } from '@/types'
5
5
  import { getMemoryDb } from '@/lib/server/memory/memory-db'
6
6
  import { loadAgents, loadSessions, saveSessions } from '@/lib/server/storage'
7
7
  import { DATA_DIR } from '@/lib/server/data-dir'
8
8
  import { isDirectConnectorSession } from '@/lib/server/connectors/session-kind'
9
+ import { getMessageCount, getRecentMessages } from '@/lib/server/messages/message-repository'
9
10
 
10
11
  const MAX_ARCHIVE_MESSAGES = 36
11
12
  const MAX_ARCHIVE_LINE_CHARS = 320
@@ -15,7 +16,7 @@ function toOneLine(value: unknown, maxChars: number): string {
15
16
  return String(value || '').replace(/\s+/g, ' ').trim().slice(0, maxChars)
16
17
  }
17
18
 
18
- function messageSpeaker(session: Session, agent: Partial<Agent> | null | undefined, message: Session['messages'][number]): string {
19
+ function messageSpeaker(session: Session, agent: Partial<Agent> | null | undefined, message: Message): string {
19
20
  if (message.role === 'assistant') return agent?.name || 'assistant'
20
21
  return (isDirectConnectorSession(session) ? session.connectorContext?.senderName : null) || session.user || 'user'
21
22
  }
@@ -38,9 +39,10 @@ export function buildSessionArchivePayload(
38
39
  references: MemoryReference[]
39
40
  hash: string
40
41
  } | null {
41
- if (!Array.isArray(session.messages) || session.messages.length < 2) return null
42
+ const messageCount = getMessageCount(session.id)
43
+ if (messageCount < 2) return null
42
44
 
43
- const excerpt = session.messages.slice(-MAX_ARCHIVE_MESSAGES).map((message) => {
45
+ const excerpt = getRecentMessages(session.id, MAX_ARCHIVE_MESSAGES).map((message) => {
44
46
  const speaker = messageSpeaker(session, agent, message)
45
47
  const kind = message.kind && message.kind !== 'chat' ? ` [${message.kind}]` : ''
46
48
  const text = toOneLine(message.text, MAX_ARCHIVE_LINE_CHARS)
@@ -57,7 +59,7 @@ export function buildSessionArchivePayload(
57
59
  `session_type: ${toOneLine(session.sessionType || 'human', 32)}`,
58
60
  `agent_name: ${toOneLine(agent?.name || '', 80)}`,
59
61
  `last_active_iso: ${new Date(session.lastActiveAt || Date.now()).toISOString()}`,
60
- `message_count: ${session.messages.length}`,
62
+ `message_count: ${messageCount}`,
61
63
  session.identityState?.personaLabel ? `persona_label: ${toOneLine(session.identityState.personaLabel, 120)}` : '',
62
64
  '',
63
65
  'Transcript excerpt:',
@@ -73,7 +75,7 @@ export function buildSessionArchivePayload(
73
75
  archiveHash: hash,
74
76
  sessionName: session.name,
75
77
  sessionType: session.sessionType || 'human',
76
- messageCount: session.messages.length,
78
+ messageCount,
77
79
  lastActiveAt: session.lastActiveAt || Date.now(),
78
80
  personaLabel: session.identityState?.personaLabel || null,
79
81
  },
@@ -93,7 +95,7 @@ export function buildSessionArchiveMarkdown(
93
95
  payload: NonNullable<ReturnType<typeof buildSessionArchivePayload>>,
94
96
  agent?: Partial<Agent> | null,
95
97
  ): string {
96
- const transcriptLines = session.messages.slice(-MAX_ARCHIVE_MESSAGES).map((message) => {
98
+ const transcriptLines = getRecentMessages(session.id, MAX_ARCHIVE_MESSAGES).map((message) => {
97
99
  const speaker = messageSpeaker(session, agent, message)
98
100
  const kind = message.kind && message.kind !== 'chat' ? ` (${message.kind})` : ''
99
101
  const toolSummary = Array.isArray(message.toolEvents) && message.toolEvents.length > 0
@@ -110,7 +112,7 @@ export function buildSessionArchiveMarkdown(
110
112
  `- Session Type: ${toOneLine(session.sessionType || 'human', 32)}`,
111
113
  `- Agent: ${toOneLine(agent?.name || session.agentId || 'unknown', 80)}`,
112
114
  `- Last Active: ${new Date(session.lastActiveAt || Date.now()).toISOString()}`,
113
- `- Messages: ${session.messages.length}`,
115
+ `- Messages: ${getMessageCount(session.id)}`,
114
116
  session.identityState?.personaLabel ? `- Persona: ${toOneLine(session.identityState.personaLabel, 120)}` : '',
115
117
  '',
116
118
  '## Archive Snapshot',
@@ -168,7 +170,7 @@ export function syncSessionArchiveMemory(
168
170
  memoryId: session.sessionArchiveState?.memoryId || existing?.id || null,
169
171
  lastHash: payload.hash,
170
172
  lastSyncedAt: session.sessionArchiveState?.lastSyncedAt || existing?.updatedAt || null,
171
- messageCount: session.messages.length,
173
+ messageCount: getMessageCount(session.id),
172
174
  exportPath: session.sessionArchiveState?.exportPath || null,
173
175
  }
174
176
  return { stored: false, memoryId: existing?.id || session.sessionArchiveState.memoryId || undefined, reason: 'unchanged' }
@@ -199,7 +201,7 @@ export function syncSessionArchiveMemory(
199
201
  memoryId: entry.id,
200
202
  lastHash: payload.hash,
201
203
  lastSyncedAt: Date.now(),
202
- messageCount: session.messages.length,
204
+ messageCount: getMessageCount(session.id),
203
205
  exportPath,
204
206
  }
205
207