@swarmclawai/swarmclaw 1.2.3 → 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 (273) hide show
  1. package/README.md +20 -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]/models/route.test.ts +60 -0
  49. package/src/app/api/providers/[id]/models/route.ts +33 -1
  50. package/src/app/api/providers/[id]/route.test.ts +49 -0
  51. package/src/app/api/providers/[id]/route.ts +30 -1
  52. package/src/app/api/providers/ollama/route.ts +6 -5
  53. package/src/app/api/schedules/[id]/route.ts +14 -108
  54. package/src/app/api/schedules/[id]/run/route.ts +6 -67
  55. package/src/app/api/schedules/route.ts +9 -51
  56. package/src/app/api/settings/route.ts +4 -3
  57. package/src/app/api/setup/check-provider/route.ts +15 -1
  58. package/src/app/api/setup/openclaw-device/route.ts +2 -2
  59. package/src/app/api/system/status/route.ts +2 -2
  60. package/src/app/api/tasks/[id]/route.ts +16 -202
  61. package/src/app/api/tasks/bulk/route.ts +5 -86
  62. package/src/app/api/tasks/metrics/route.ts +2 -1
  63. package/src/app/api/tasks/route.ts +11 -171
  64. package/src/app/api/upload/route.ts +1 -1
  65. package/src/app/api/uploads/[filename]/route.ts +1 -1
  66. package/src/app/api/uploads/route.ts +1 -1
  67. package/src/app/api/webhooks/[id]/history/route.ts +2 -2
  68. package/src/app/layout.tsx +9 -6
  69. package/src/app/protocols/page.tsx +71 -89
  70. package/src/app/tasks/page.tsx +32 -32
  71. package/src/cli/index.js +1 -0
  72. package/src/cli/spec.js +1 -0
  73. package/src/components/agents/agent-sheet.tsx +51 -25
  74. package/src/components/agents/inspector-panel.tsx +15 -4
  75. package/src/components/auth/setup-wizard/index.tsx +27 -18
  76. package/src/components/auth/setup-wizard/shared.tsx +2 -2
  77. package/src/components/auth/setup-wizard/step-agents.tsx +51 -38
  78. package/src/components/auth/setup-wizard/step-connect.tsx +48 -17
  79. package/src/components/auth/setup-wizard/types.ts +6 -4
  80. package/src/components/auth/setup-wizard/utils.test.ts +38 -8
  81. package/src/components/auth/setup-wizard/utils.ts +14 -8
  82. package/src/components/chatrooms/chatroom-sheet.tsx +16 -276
  83. package/src/components/connectors/connector-list.tsx +26 -40
  84. package/src/components/connectors/connector-sheet.tsx +95 -149
  85. package/src/components/gateways/gateway-sheet.tsx +61 -110
  86. package/src/components/layout/live-query-sync.tsx +121 -0
  87. package/src/components/protocols/structured-session-launcher.tsx +24 -45
  88. package/src/components/providers/app-query-provider.tsx +17 -0
  89. package/src/components/providers/provider-list.tsx +150 -77
  90. package/src/components/providers/provider-sheet.tsx +102 -77
  91. package/src/components/shared/model-combobox.tsx +5 -4
  92. package/src/components/skills/skill-list.tsx +5 -18
  93. package/src/components/skills/skill-sheet.tsx +21 -20
  94. package/src/components/skills/skills-workspace.tsx +48 -87
  95. package/src/components/tasks/task-card.tsx +20 -13
  96. package/src/components/tasks/task-column.tsx +22 -7
  97. package/src/components/tasks/task-list.tsx +8 -11
  98. package/src/components/tasks/task-sheet.tsx +111 -103
  99. package/src/features/agents/queries.ts +20 -0
  100. package/src/features/chatrooms/queries.ts +20 -0
  101. package/src/features/chats/queries.ts +27 -0
  102. package/src/features/connectors/queries.ts +145 -0
  103. package/src/features/credentials/queries.ts +37 -0
  104. package/src/features/extensions/queries.ts +26 -0
  105. package/src/features/external-agents/queries.ts +36 -0
  106. package/src/features/gateways/queries.ts +274 -0
  107. package/src/features/missions/queries.ts +23 -0
  108. package/src/features/projects/queries.ts +20 -0
  109. package/src/features/protocols/queries.ts +149 -0
  110. package/src/features/providers/queries.ts +142 -0
  111. package/src/features/settings/queries.ts +20 -0
  112. package/src/features/skills/queries.ts +182 -0
  113. package/src/features/tasks/queries.ts +189 -0
  114. package/src/hooks/use-ws.ts +3 -2
  115. package/src/lib/agent-provider-options.test.ts +152 -0
  116. package/src/lib/agent-provider-options.ts +84 -0
  117. package/src/lib/app/api-client.ts +2 -2
  118. package/src/lib/providers/index.test.ts +78 -0
  119. package/src/lib/providers/index.ts +13 -10
  120. package/src/lib/query/client.ts +17 -0
  121. package/src/lib/server/agents/agent-runtime-config.ts +6 -6
  122. package/src/lib/server/agents/agent-service.ts +429 -0
  123. package/src/lib/server/agents/agent-thread-session.ts +6 -5
  124. package/src/lib/server/agents/autonomy-contract.ts +1 -4
  125. package/src/lib/server/agents/delegation-advisory.test.ts +206 -0
  126. package/src/lib/server/agents/delegation-advisory.ts +251 -0
  127. package/src/lib/server/agents/main-agent-loop.ts +98 -40
  128. package/src/lib/server/agents/subagent-runtime.ts +12 -0
  129. package/src/lib/server/autonomy/supervisor-reflection.test.ts +20 -1
  130. package/src/lib/server/autonomy/supervisor-reflection.ts +39 -19
  131. package/src/lib/server/build-llm.ts +7 -15
  132. package/src/lib/server/capability-router.test.ts +70 -1
  133. package/src/lib/server/capability-router.ts +24 -99
  134. package/src/lib/server/chat-execution/chat-execution-utils.ts +0 -15
  135. package/src/lib/server/chat-execution/chat-streaming-utils.ts +2 -4
  136. package/src/lib/server/chat-execution/chat-turn-finalization.ts +77 -12
  137. package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +4 -4
  138. package/src/lib/server/chat-execution/chat-turn-preflight.ts +2 -2
  139. package/src/lib/server/chat-execution/chat-turn-preparation.ts +41 -17
  140. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +4 -2
  141. package/src/lib/server/chat-execution/chat-turn-tool-routing.test.ts +45 -0
  142. package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +48 -17
  143. package/src/lib/server/chat-execution/continuation-evaluator.ts +4 -1
  144. package/src/lib/server/chat-execution/direct-memory-intent.test.ts +9 -0
  145. package/src/lib/server/chat-execution/direct-memory-intent.ts +12 -2
  146. package/src/lib/server/chat-execution/message-classifier.test.ts +35 -23
  147. package/src/lib/server/chat-execution/message-classifier.ts +74 -32
  148. package/src/lib/server/chat-execution/prompt-builder.test.ts +29 -0
  149. package/src/lib/server/chat-execution/prompt-builder.ts +37 -2
  150. package/src/lib/server/chat-execution/prompt-sections.test.ts +56 -0
  151. package/src/lib/server/chat-execution/prompt-sections.ts +193 -0
  152. package/src/lib/server/chat-execution/stream-agent-chat.ts +63 -7
  153. package/src/lib/server/chat-execution/stream-continuation.test.ts +36 -0
  154. package/src/lib/server/chat-execution/stream-continuation.ts +28 -13
  155. package/src/lib/server/chatrooms/chatroom-agent-signals.ts +26 -18
  156. package/src/lib/server/chatrooms/chatroom-helpers.ts +19 -18
  157. package/src/lib/server/chatrooms/chatroom-repository.ts +16 -0
  158. package/src/lib/server/chatrooms/chatroom-routing.test.ts +96 -0
  159. package/src/lib/server/chatrooms/chatroom-routing.ts +207 -53
  160. package/src/lib/server/chatrooms/mailbox-utils.ts +4 -2
  161. package/src/lib/server/chatrooms/session-mailbox.ts +50 -40
  162. package/src/lib/server/chats/chat-session-service.ts +410 -0
  163. package/src/lib/server/connectors/access.ts +1 -1
  164. package/src/lib/server/connectors/commands.ts +7 -6
  165. package/src/lib/server/connectors/connector-inbound.ts +14 -7
  166. package/src/lib/server/connectors/connector-outbound.ts +16 -11
  167. package/src/lib/server/connectors/connector-service.ts +453 -0
  168. package/src/lib/server/connectors/delivery.ts +17 -12
  169. package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -14
  170. package/src/lib/server/connectors/media.ts +1 -1
  171. package/src/lib/server/connectors/response-media.ts +1 -1
  172. package/src/lib/server/connectors/session-consolidation.ts +11 -7
  173. package/src/lib/server/connectors/session.ts +9 -7
  174. package/src/lib/server/connectors/voice-note.ts +2 -1
  175. package/src/lib/server/context-manager.ts +20 -1
  176. package/src/lib/server/cost.ts +2 -3
  177. package/src/lib/server/credentials/credential-repository.ts +43 -4
  178. package/src/lib/server/credentials/credential-service.ts +112 -0
  179. package/src/lib/server/daemon/admin-metadata.ts +64 -0
  180. package/src/lib/server/daemon/controller.ts +577 -0
  181. package/src/lib/server/daemon/daemon-runtime.ts +352 -0
  182. package/src/lib/server/daemon/daemon-status-repository.ts +63 -0
  183. package/src/lib/server/daemon/types.ts +101 -0
  184. package/src/lib/server/embeddings.ts +3 -9
  185. package/src/lib/server/eval/agent-regression.ts +3 -2
  186. package/src/lib/server/eval/runner.ts +2 -2
  187. package/src/lib/server/execution-brief.test.ts +167 -0
  188. package/src/lib/server/execution-brief.ts +295 -0
  189. package/src/lib/server/execution-engine/chat-turn.ts +9 -0
  190. package/src/lib/server/execution-engine/import-boundary.test.ts +44 -0
  191. package/src/lib/server/execution-engine/index.ts +35 -0
  192. package/src/lib/server/execution-engine/task-attempt.ts +303 -0
  193. package/src/lib/server/execution-engine/types.ts +33 -0
  194. package/src/lib/server/gateways/gateway-profile-repository.ts +47 -3
  195. package/src/lib/server/gateways/gateway-profile-service.ts +200 -0
  196. package/src/lib/server/memory/session-archive-memory.ts +12 -10
  197. package/src/lib/server/messages/message-repository.ts +330 -0
  198. package/src/lib/server/missions/mission-service/core.ts +8 -6
  199. package/src/lib/server/openclaw/agent-resolver.ts +2 -3
  200. package/src/lib/server/openclaw/doctor.ts +1 -1
  201. package/src/lib/server/openclaw/gateway.test.ts +10 -1
  202. package/src/lib/server/openclaw/gateway.ts +5 -14
  203. package/src/lib/server/openclaw/health.ts +3 -11
  204. package/src/lib/server/openclaw/sync.ts +8 -6
  205. package/src/lib/server/persistence/storage-context.ts +3 -0
  206. package/src/lib/server/protocols/protocol-agent-turn.ts +25 -17
  207. package/src/lib/server/protocols/protocol-normalization.ts +1 -1
  208. package/src/lib/server/protocols/protocol-queries.ts +13 -7
  209. package/src/lib/server/protocols/protocol-run-lifecycle.ts +16 -20
  210. package/src/lib/server/protocols/protocol-run-repository.ts +81 -0
  211. package/src/lib/server/protocols/protocol-step-processors.ts +23 -31
  212. package/src/lib/server/protocols/protocol-swarm.ts +8 -8
  213. package/src/lib/server/protocols/protocol-template-repository.ts +42 -0
  214. package/src/lib/server/protocols/protocol-templates.ts +4 -2
  215. package/src/lib/server/protocols/protocol-types.ts +10 -7
  216. package/src/lib/server/provider-endpoint.ts +7 -12
  217. package/src/lib/server/provider-model-discovery.ts +2 -11
  218. package/src/lib/server/query-expansion.ts +5 -6
  219. package/src/lib/server/run-context.test.ts +365 -0
  220. package/src/lib/server/run-context.ts +367 -0
  221. package/src/lib/server/runtime/heartbeat-service.ts +7 -5
  222. package/src/lib/server/runtime/queue/core.ts +61 -190
  223. package/src/lib/server/runtime/run-ledger.ts +8 -0
  224. package/src/lib/server/runtime/session-run-manager/drain.ts +2 -2
  225. package/src/lib/server/runtime/session-run-manager/enqueue.ts +6 -0
  226. package/src/lib/server/runtime/session-run-manager/state.ts +4 -0
  227. package/src/lib/server/schedules/schedule-route-service.ts +230 -0
  228. package/src/lib/server/service-result.ts +16 -0
  229. package/src/lib/server/session-note.ts +2 -3
  230. package/src/lib/server/session-reset-policy.ts +4 -3
  231. package/src/lib/server/session-tools/connector.ts +9 -6
  232. package/src/lib/server/session-tools/context-mgmt.ts +58 -9
  233. package/src/lib/server/session-tools/crud.ts +162 -10
  234. package/src/lib/server/session-tools/delegate.ts +1 -1
  235. package/src/lib/server/session-tools/manage-tasks.test.ts +152 -0
  236. package/src/lib/server/session-tools/memory.ts +6 -4
  237. package/src/lib/server/session-tools/session-info.test.ts +56 -0
  238. package/src/lib/server/session-tools/session-info.ts +119 -12
  239. package/src/lib/server/session-tools/skill-runtime.ts +3 -1
  240. package/src/lib/server/session-tools/skills.ts +15 -15
  241. package/src/lib/server/session-tools/subagent.test.ts +115 -1
  242. package/src/lib/server/session-tools/subagent.ts +125 -7
  243. package/src/lib/server/session-tools/team-context.ts +4 -3
  244. package/src/lib/server/session-tools/wallet.ts +0 -58
  245. package/src/lib/server/sessions/session-lineage.ts +55 -0
  246. package/src/lib/server/sessions/session-repository.ts +2 -2
  247. package/src/lib/server/skills/learned-skills.ts +24 -23
  248. package/src/lib/server/skills/runtime-skill-resolver.ts +2 -1
  249. package/src/lib/server/skills/skill-repository.ts +136 -13
  250. package/src/lib/server/skills/skill-suggestions.ts +25 -28
  251. package/src/lib/server/storage-normalization.test.ts +42 -215
  252. package/src/lib/server/storage-normalization.ts +98 -0
  253. package/src/lib/server/storage.ts +19 -0
  254. package/src/lib/server/structured-extract.ts +3 -14
  255. package/src/lib/server/tasks/task-followups.ts +16 -11
  256. package/src/lib/server/tasks/task-result.test.ts +25 -29
  257. package/src/lib/server/tasks/task-result.ts +5 -9
  258. package/src/lib/server/tasks/task-route-service.ts +449 -0
  259. package/src/lib/server/text-normalization.ts +41 -0
  260. package/src/lib/server/tool-planning.ts +6 -42
  261. package/src/lib/server/upload-path.ts +5 -0
  262. package/src/lib/server/working-state/extraction.ts +614 -0
  263. package/src/lib/server/working-state/normalization.ts +866 -0
  264. package/src/lib/server/working-state/prompt.ts +60 -0
  265. package/src/lib/server/working-state/repository.ts +38 -0
  266. package/src/lib/server/working-state/service.test.ts +253 -0
  267. package/src/lib/server/working-state/service.ts +293 -0
  268. package/src/lib/validation/schemas.ts +1 -0
  269. package/src/lib/ws-client.ts +3 -3
  270. package/src/stores/slices/task-slice.ts +1 -4
  271. package/src/stores/use-chatroom-store.ts +2 -2
  272. package/src/types/index.ts +288 -22
  273. package/src/views/settings/section-providers.tsx +2 -2
@@ -3,8 +3,10 @@ import path from 'path'
3
3
  import { ImapFlow } from 'imapflow'
4
4
  import { createTransport } from 'nodemailer'
5
5
  import { simpleParser } from 'mailparser'
6
- import { UPLOAD_DIR, loadConnectors } from '@/lib/server/storage'
6
+ import type { Connector } from '@/types'
7
+ import { loadConnectors } from '@/lib/server/connectors/connector-repository'
7
8
  import { getExtensionManager } from '@/lib/server/extensions'
9
+ import { UPLOAD_DIR } from '@/lib/server/upload-path'
8
10
 
9
11
  export interface MailboxConfig {
10
12
  imapHost: string
@@ -79,7 +81,7 @@ export function getMailboxConfig(): MailboxConfig {
79
81
  const emailSettings = extensionManager.getExtensionSettings('email') as Record<string, unknown>
80
82
  const connectors = loadConnectors()
81
83
  const emailConnector = Object.values(connectors)
82
- .find((entry) => entry && typeof entry === 'object' && String((entry as Record<string, unknown>).platform || '').toLowerCase() === 'email') as Record<string, unknown> | undefined
84
+ .find((entry) => entry.platform === 'email') as Connector | undefined
83
85
  const connectorConfig = emailConnector && typeof emailConnector.config === 'object' && emailConnector.config
84
86
  ? emailConnector.config as Record<string, unknown>
85
87
  : {}
@@ -1,6 +1,6 @@
1
1
  import { genId } from '@/lib/id'
2
2
  import type { MailboxEnvelope } from '@/types'
3
- import { loadSessions, saveSessions } from '@/lib/server/storage'
3
+ import { loadSession, patchSession } from '@/lib/server/sessions/session-repository'
4
4
  import { requestMissionTicksForHumanReply } from '@/lib/server/missions/mission-service'
5
5
 
6
6
  interface MailboxOptions {
@@ -83,9 +83,8 @@ function normalizeMailbox(target: { mailbox?: MailboxEnvelope[] | null }, now =
83
83
 
84
84
  function findLatestPendingHumanRequestEnvelope(
85
85
  sessionId: string,
86
- sessions = loadSessions(),
86
+ target = loadSession(sessionId),
87
87
  ): MailboxEnvelope | null {
88
- const target = sessions[sessionId]
89
88
  if (!target) throw new Error(`Session not found: ${sessionId}`)
90
89
  const envelopes = normalizeMailbox(target)
91
90
  const repliedCorrelationIds = new Set(
@@ -108,8 +107,7 @@ export function findPendingHumanRequestEnvelope(params: {
108
107
  fromSessionId?: string | null
109
108
  fromAgentId?: string | null
110
109
  }): MailboxEnvelope | null {
111
- const sessions = loadSessions()
112
- const target = sessions[params.sessionId]
110
+ const target = loadSession(params.sessionId)
113
111
  if (!target) throw new Error(`Session not found: ${params.sessionId}`)
114
112
  const expectedSignature = normalizeHumanRequestSignature(params)
115
113
  const envelopes = normalizeMailbox(target)
@@ -139,8 +137,7 @@ export function sendMailboxEnvelope(input: {
139
137
  correlationId?: string | null
140
138
  ttlSec?: number | null
141
139
  }): MailboxEnvelope {
142
- const sessions = loadSessions()
143
- const target = sessions[input.toSessionId]
140
+ const target = loadSession(input.toSessionId)
144
141
  if (!target) throw new Error(`Target session not found: ${input.toSessionId}`)
145
142
 
146
143
  const now = Date.now()
@@ -162,12 +159,14 @@ export function sendMailboxEnvelope(input: {
162
159
  ackAt: null,
163
160
  }
164
161
 
165
- const existing = normalizeMailbox(target, now)
166
- existing.push(envelope)
167
- target.mailbox = existing
168
- target.lastActiveAt = now
169
- sessions[input.toSessionId] = target
170
- saveSessions(sessions)
162
+ patchSession(input.toSessionId, (current) => {
163
+ if (!current) return null
164
+ return {
165
+ ...current,
166
+ mailbox: [...normalizeMailbox(current, now), envelope],
167
+ lastActiveAt: now,
168
+ }
169
+ })
171
170
  if (envelope.type === 'human_reply') {
172
171
  requestMissionTicksForHumanReply({
173
172
  sessionId: input.toSessionId,
@@ -188,8 +187,7 @@ export function sendMailboxEnvelope(input: {
188
187
  }
189
188
 
190
189
  export function listMailbox(sessionId: string, opts: MailboxOptions = {}): MailboxEnvelope[] {
191
- const sessions = loadSessions()
192
- const target = sessions[sessionId]
190
+ const target = loadSession(sessionId)
193
191
  if (!target) throw new Error(`Session not found: ${sessionId}`)
194
192
  const list = pruneExpired(normalizeMailboxList(target.mailbox || []))
195
193
  const includeAcked = opts.includeAcked === true
@@ -201,36 +199,48 @@ export function listMailbox(sessionId: string, opts: MailboxOptions = {}): Mailb
201
199
  }
202
200
 
203
201
  export function ackMailboxEnvelope(sessionId: string, envelopeId: string): MailboxEnvelope | null {
204
- const sessions = loadSessions()
205
- const target = sessions[sessionId]
202
+ const target = loadSession(sessionId)
206
203
  if (!target) throw new Error(`Session not found: ${sessionId}`)
207
- const list = normalizeMailbox(target)
208
- const idx = list.findIndex((env) => env.id === envelopeId)
209
- if (idx === -1) return null
210
- list[idx] = {
211
- ...list[idx],
212
- status: 'ack',
213
- ackAt: Date.now(),
214
- }
215
- target.mailbox = list
216
- target.lastActiveAt = Date.now()
217
- sessions[sessionId] = target
218
- saveSessions(sessions)
219
- return list[idx]
204
+ const ackAt = Date.now()
205
+ let acked: MailboxEnvelope | null = null
206
+ patchSession(sessionId, (current) => {
207
+ if (!current) return null
208
+ const list = normalizeMailbox(current)
209
+ const idx = list.findIndex((env) => env.id === envelopeId)
210
+ if (idx === -1) return current
211
+ list[idx] = {
212
+ ...list[idx],
213
+ status: 'ack',
214
+ ackAt,
215
+ }
216
+ acked = list[idx]
217
+ return {
218
+ ...current,
219
+ mailbox: list,
220
+ lastActiveAt: ackAt,
221
+ }
222
+ })
223
+ return acked
220
224
  }
221
225
 
222
226
  export function clearMailbox(sessionId: string, includeAcked = true): { before: number; after: number } {
223
- const sessions = loadSessions()
224
- const target = sessions[sessionId]
227
+ const target = loadSession(sessionId)
225
228
  if (!target) throw new Error(`Session not found: ${sessionId}`)
226
- const list = normalizeMailbox(target)
227
- const before = list.length
228
- const afterList = includeAcked ? [] : list.filter((env) => env.status !== 'ack')
229
- target.mailbox = afterList
230
- target.lastActiveAt = Date.now()
231
- sessions[sessionId] = target
232
- saveSessions(sessions)
233
- return { before, after: afterList.length }
229
+ let before = 0
230
+ let after = 0
231
+ patchSession(sessionId, (current) => {
232
+ if (!current) return null
233
+ const list = normalizeMailbox(current)
234
+ const afterList = includeAcked ? [] : list.filter((env) => env.status !== 'ack')
235
+ before = list.length
236
+ after = afterList.length
237
+ return {
238
+ ...current,
239
+ mailbox: afterList,
240
+ lastActiveAt: Date.now(),
241
+ }
242
+ })
243
+ return { before, after }
234
244
  }
235
245
 
236
246
  export function bridgeHumanReplyFromChat(input: {
@@ -0,0 +1,410 @@
1
+ import os from 'node:os'
2
+ import path from 'node:path'
3
+
4
+ import { genId } from '@/lib/id'
5
+ import { normalizeCapabilitySelection } from '@/lib/capability-selection'
6
+ import { buildAgentDisabledMessage, isAgentDisabled } from '@/lib/server/agents/agent-availability'
7
+ import { loadAgent } from '@/lib/server/agents/agent-repository'
8
+ import { clearMainLoopStateForSession } from '@/lib/server/agents/main-agent-loop'
9
+ import { applyResolvedRoute, resolvePrimaryAgentRoute } from '@/lib/server/agents/agent-runtime-config'
10
+ import { enrichSessionWithMissionSummary } from '@/lib/server/missions/mission-service'
11
+ import { cleanupSessionProcesses } from '@/lib/server/runtime/process-manager'
12
+ import { stopActiveSessionProcess } from '@/lib/server/runtime/runtime-state'
13
+ import {
14
+ cancelQueuedRunById,
15
+ cancelQueuedRunsForSession,
16
+ enqueueSessionRun,
17
+ getSessionQueueSnapshot,
18
+ getSessionRunState,
19
+ } from '@/lib/server/runtime/session-run-manager'
20
+ import { deleteSession, getSession, listSessions, saveSession } from '@/lib/server/sessions/session-repository'
21
+ import {
22
+ clearMessages,
23
+ deleteSessionMessages,
24
+ getMessages,
25
+ truncateAfter,
26
+ } from '@/lib/server/messages/message-repository'
27
+ import { deleteSessionWorkingState } from '@/lib/server/working-state/service'
28
+ import { normalizeProviderEndpoint } from '@/lib/openclaw/openclaw-endpoint'
29
+ import { serviceFail, serviceOk } from '@/lib/server/service-result'
30
+ import { WORKSPACE_DIR } from '@/lib/server/data-dir'
31
+ import { buildSessionListSummary } from '@/lib/chat/session-summary'
32
+ import type { Session } from '@/types'
33
+ import type { ServiceResult } from '@/lib/server/service-result'
34
+ import { notify } from '@/lib/server/ws-hub'
35
+
36
+ function normalizeCwd(value: unknown): string {
37
+ const raw = typeof value === 'string' ? value.trim() : ''
38
+ if (raw.startsWith('~/')) return path.join(os.homedir(), raw.slice(2))
39
+ if (raw === '~') return os.homedir()
40
+ if (!raw) return WORKSPACE_DIR
41
+ return raw
42
+ }
43
+
44
+ function emptyDelegateResumeIds() {
45
+ return {
46
+ claudeCode: null,
47
+ codex: null,
48
+ opencode: null,
49
+ gemini: null,
50
+ }
51
+ }
52
+
53
+ export function listChatsForApi(): Record<string, ReturnType<typeof buildSessionListSummary>> {
54
+ const sessions = listSessions()
55
+ for (const id of Object.keys(sessions)) {
56
+ const run = getSessionRunState(id)
57
+ const queue = getSessionQueueSnapshot(id)
58
+ sessions[id].active = !!run.runningRunId
59
+ sessions[id].queuedCount = queue.queueLength
60
+ sessions[id].currentRunId = run.runningRunId || null
61
+ }
62
+ return Object.fromEntries(
63
+ Object.entries(sessions).map(([id, session]) => [id, buildSessionListSummary(enrichSessionWithMissionSummary(session))]),
64
+ )
65
+ }
66
+
67
+ export function getChatSessionForApi(sessionId: string): Session | null {
68
+ const session = getSession(sessionId)
69
+ if (!session) return null
70
+ const run = getSessionRunState(sessionId)
71
+ const queue = getSessionQueueSnapshot(sessionId)
72
+ session.active = !!run.runningRunId
73
+ session.queuedCount = queue.queueLength
74
+ session.currentRunId = run.runningRunId || null
75
+ return enrichSessionWithMissionSummary(session)
76
+ }
77
+
78
+ export function createChatSession(input: Record<string, unknown>): ServiceResult<Session> {
79
+ const id = typeof input.id === 'string' && input.id.trim() ? input.id.trim() : genId()
80
+ const sessions = listSessions()
81
+ if (typeof input.id === 'string' && sessions[id]) {
82
+ return serviceOk(sessions[id])
83
+ }
84
+ const agent = typeof input.agentId === 'string' ? loadAgent(input.agentId) : null
85
+ if (isAgentDisabled(agent)) {
86
+ return serviceFail(409, buildAgentDisabledMessage(agent, 'start chats'))
87
+ }
88
+ const explicitOllamaMode = input.ollamaMode === 'cloud' ? 'cloud' : input.ollamaMode === 'local' ? 'local' : null
89
+ const routePreferredGatewayTags = Array.isArray(input.routePreferredGatewayTags)
90
+ ? input.routePreferredGatewayTags.filter((tag): tag is string => typeof tag === 'string' && tag.trim().length > 0)
91
+ : []
92
+ const routePreferredGatewayUseCase = typeof input.routePreferredGatewayUseCase === 'string' && input.routePreferredGatewayUseCase.trim()
93
+ ? input.routePreferredGatewayUseCase.trim()
94
+ : null
95
+ const resolvedRoute = agent ? resolvePrimaryAgentRoute(agent, undefined, {
96
+ preferredGatewayTags: routePreferredGatewayTags,
97
+ preferredGatewayUseCase: routePreferredGatewayUseCase,
98
+ }) : null
99
+ const resolvedCapabilities = normalizeCapabilitySelection({
100
+ tools: Array.isArray(input.tools) ? input.tools : agent?.tools,
101
+ extensions: Array.isArray(input.extensions) ? input.extensions : agent?.extensions,
102
+ })
103
+ const provider = (
104
+ typeof input.provider === 'string' && input.provider.trim()
105
+ ? input.provider.trim()
106
+ : agent?.provider || 'claude-cli'
107
+ ) as Session['provider']
108
+ const now = Date.now()
109
+ const baseSession: Session = {
110
+ id,
111
+ name: (input.name as string) || 'New Chat',
112
+ cwd: normalizeCwd(input.cwd),
113
+ user: (input.user as string) || 'user',
114
+ provider,
115
+ model: (input.model as string) || agent?.model || '',
116
+ ollamaMode: explicitOllamaMode ?? agent?.ollamaMode ?? (provider === 'ollama' ? 'local' : null),
117
+ credentialId: (input.credentialId as string | null | undefined) || agent?.credentialId || null,
118
+ fallbackCredentialIds: Array.isArray(input.fallbackCredentialIds) ? input.fallbackCredentialIds : agent?.fallbackCredentialIds || [],
119
+ apiEndpoint: normalizeProviderEndpoint(
120
+ provider,
121
+ (input.apiEndpoint as string | null | undefined) || agent?.apiEndpoint || null,
122
+ ),
123
+ routePreferredGatewayTags,
124
+ routePreferredGatewayUseCase,
125
+ claudeSessionId: null,
126
+ codexThreadId: null,
127
+ opencodeSessionId: null,
128
+ delegateResumeIds: emptyDelegateResumeIds(),
129
+ messages: Array.isArray(input.messages) ? input.messages : [],
130
+ createdAt: now,
131
+ lastActiveAt: now,
132
+ sessionType: (input.sessionType as Session['sessionType']) || 'human',
133
+ agentId: (input.agentId as string | null | undefined) || null,
134
+ parentSessionId: (input.parentSessionId as string | null | undefined) || null,
135
+ tools: resolvedCapabilities.tools,
136
+ extensions: resolvedCapabilities.extensions,
137
+ heartbeatEnabled: (input.heartbeatEnabled as boolean | null | undefined) ?? null,
138
+ heartbeatIntervalSec: (input.heartbeatIntervalSec as number | null | undefined) ?? null,
139
+ sessionResetMode: (input.sessionResetMode as Session['sessionResetMode']) ?? agent?.sessionResetMode ?? null,
140
+ sessionIdleTimeoutSec: (input.sessionIdleTimeoutSec as number | null | undefined) ?? agent?.sessionIdleTimeoutSec ?? null,
141
+ sessionMaxAgeSec: (input.sessionMaxAgeSec as number | null | undefined) ?? agent?.sessionMaxAgeSec ?? null,
142
+ sessionDailyResetAt: (input.sessionDailyResetAt as string | null | undefined) ?? agent?.sessionDailyResetAt ?? null,
143
+ sessionResetTimezone: (input.sessionResetTimezone as string | null | undefined) ?? agent?.sessionResetTimezone ?? null,
144
+ thinkingLevel: (input.thinkingLevel as Session['thinkingLevel']) ?? null,
145
+ connectorThinkLevel: (input.connectorThinkLevel as Session['connectorThinkLevel']) ?? null,
146
+ connectorSessionScope: (input.connectorSessionScope as Session['connectorSessionScope']) ?? null,
147
+ connectorReplyMode: (input.connectorReplyMode as Session['connectorReplyMode']) ?? null,
148
+ connectorThreadBinding: (input.connectorThreadBinding as Session['connectorThreadBinding']) ?? null,
149
+ connectorGroupPolicy: (input.connectorGroupPolicy as Session['connectorGroupPolicy']) ?? null,
150
+ connectorIdleTimeoutSec: (input.connectorIdleTimeoutSec as number | null | undefined) ?? null,
151
+ connectorMaxAgeSec: (input.connectorMaxAgeSec as number | null | undefined) ?? null,
152
+ connectorContext: input.connectorContext === null
153
+ ? undefined
154
+ : (input.connectorContext as Session['connectorContext']),
155
+ identityState: (input.identityState as Session['identityState']) ?? agent?.identityState ?? null,
156
+ sessionArchiveState: (input.sessionArchiveState as Session['sessionArchiveState']) ?? null,
157
+ }
158
+ const session: Session = (input.provider || input.model || input.credentialId || input.apiEndpoint)
159
+ ? baseSession
160
+ : applyResolvedRoute(baseSession, resolvedRoute)
161
+ saveSession(id, session)
162
+ notify('sessions')
163
+ return serviceOk(session)
164
+ }
165
+
166
+ export function deleteChats(ids: string[]): { deleted: number; requested: number } {
167
+ let deleted = 0
168
+ const sessions = listSessions()
169
+ for (const id of ids) {
170
+ if (!sessions[id]) continue
171
+ stopActiveSessionProcess(id)
172
+ deleteSessionWorkingState(id)
173
+ clearMainLoopStateForSession(id)
174
+ deleteSessionMessages(id)
175
+ deleteSession(id)
176
+ deleted += 1
177
+ }
178
+ if (deleted > 0) notify('sessions')
179
+ return { deleted, requested: ids.length }
180
+ }
181
+
182
+ export function updateChatSession(sessionId: string, updates: Record<string, unknown>): Session | null {
183
+ const original = getSession(sessionId)
184
+ if (!original) return null
185
+ const session = original as unknown as Record<string, unknown>
186
+
187
+ if (updates.resetMainLoopState === true) {
188
+ clearMainLoopStateForSession(sessionId)
189
+ deleteSessionWorkingState(sessionId)
190
+ }
191
+
192
+ const agentIdUpdateProvided = updates.agentId !== undefined
193
+ let nextAgentId = session.agentId
194
+ if (agentIdUpdateProvided) {
195
+ session.agentId = updates.agentId
196
+ nextAgentId = updates.agentId
197
+ }
198
+
199
+ const linkedAgent = nextAgentId ? loadAgent(String(nextAgentId)) : null
200
+ const routePreferredGatewayTags = updates.routePreferredGatewayTags !== undefined
201
+ ? (Array.isArray(updates.routePreferredGatewayTags)
202
+ ? updates.routePreferredGatewayTags.filter((tag): tag is string => typeof tag === 'string' && tag.trim().length > 0)
203
+ : [])
204
+ : ((session.routePreferredGatewayTags as string[]) || [])
205
+ const routePreferredGatewayUseCase = updates.routePreferredGatewayUseCase !== undefined
206
+ ? (typeof updates.routePreferredGatewayUseCase === 'string' && updates.routePreferredGatewayUseCase.trim()
207
+ ? updates.routePreferredGatewayUseCase.trim()
208
+ : null)
209
+ : ((session.routePreferredGatewayUseCase as string | null) || null)
210
+ const linkedRoute = linkedAgent ? resolvePrimaryAgentRoute(linkedAgent, undefined, {
211
+ preferredGatewayTags: routePreferredGatewayTags,
212
+ preferredGatewayUseCase: routePreferredGatewayUseCase,
213
+ }) : null
214
+
215
+ if (updates.name !== undefined) session.name = updates.name
216
+ if (updates.cwd !== undefined) session.cwd = normalizeCwd(updates.cwd)
217
+ if (updates.provider !== undefined) session.provider = updates.provider
218
+ else if (agentIdUpdateProvided && linkedAgent?.provider) session.provider = linkedAgent.provider
219
+ if (updates.model !== undefined) session.model = updates.model
220
+ else if (agentIdUpdateProvided && linkedRoute?.model) session.model = linkedRoute.model
221
+ else if (agentIdUpdateProvided && linkedAgent?.model !== undefined) session.model = linkedAgent.model
222
+ if (updates.ollamaMode !== undefined) session.ollamaMode = updates.ollamaMode
223
+ else if (updates.provider !== undefined && updates.provider !== 'ollama') session.ollamaMode = null
224
+ else if (agentIdUpdateProvided && linkedRoute) session.ollamaMode = linkedRoute.ollamaMode ?? null
225
+ else if (agentIdUpdateProvided && linkedAgent) session.ollamaMode = linkedAgent.ollamaMode ?? null
226
+ if (updates.credentialId !== undefined) session.credentialId = updates.credentialId
227
+ else if (agentIdUpdateProvided && linkedRoute) session.credentialId = linkedRoute.credentialId ?? null
228
+ else if (agentIdUpdateProvided && linkedAgent) session.credentialId = linkedAgent.credentialId ?? null
229
+ if (updates.fallbackCredentialIds !== undefined) session.fallbackCredentialIds = updates.fallbackCredentialIds
230
+ else if (agentIdUpdateProvided && linkedRoute) session.fallbackCredentialIds = [...linkedRoute.fallbackCredentialIds]
231
+ if (updates.gatewayProfileId !== undefined) session.gatewayProfileId = updates.gatewayProfileId
232
+ else if (agentIdUpdateProvided && linkedRoute) session.gatewayProfileId = linkedRoute.gatewayProfileId ?? null
233
+ if (updates.routePreferredGatewayTags !== undefined) session.routePreferredGatewayTags = routePreferredGatewayTags
234
+ if (updates.routePreferredGatewayUseCase !== undefined) session.routePreferredGatewayUseCase = routePreferredGatewayUseCase
235
+
236
+ if (updates.tools !== undefined || updates.extensions !== undefined || (agentIdUpdateProvided && linkedAgent)) {
237
+ const nextSelection = normalizeCapabilitySelection({
238
+ tools: Array.isArray(updates.tools)
239
+ ? updates.tools
240
+ : (agentIdUpdateProvided && linkedAgent ? linkedAgent.tools : session.tools as string[] | undefined),
241
+ extensions: Array.isArray(updates.extensions)
242
+ ? updates.extensions
243
+ : (agentIdUpdateProvided && linkedAgent ? linkedAgent.extensions : session.extensions as string[] | undefined),
244
+ })
245
+ session.tools = nextSelection.tools
246
+ session.extensions = nextSelection.extensions
247
+ }
248
+
249
+ if (updates.apiEndpoint !== undefined) {
250
+ session.apiEndpoint = normalizeProviderEndpoint(
251
+ (updates.provider || session.provider) as string,
252
+ updates.apiEndpoint as string | null | undefined,
253
+ )
254
+ } else if (agentIdUpdateProvided && linkedRoute) {
255
+ session.apiEndpoint = linkedRoute.apiEndpoint ?? null
256
+ } else if (agentIdUpdateProvided && linkedAgent) {
257
+ session.apiEndpoint = normalizeProviderEndpoint(linkedAgent.provider, linkedAgent.apiEndpoint ?? null)
258
+ }
259
+ if (updates.heartbeatEnabled !== undefined) session.heartbeatEnabled = updates.heartbeatEnabled
260
+ if (updates.heartbeatIntervalSec !== undefined) session.heartbeatIntervalSec = updates.heartbeatIntervalSec
261
+ if (updates.sessionResetMode !== undefined) session.sessionResetMode = updates.sessionResetMode
262
+ if (updates.sessionIdleTimeoutSec !== undefined) session.sessionIdleTimeoutSec = updates.sessionIdleTimeoutSec
263
+ if (updates.sessionMaxAgeSec !== undefined) session.sessionMaxAgeSec = updates.sessionMaxAgeSec
264
+ if (updates.sessionDailyResetAt !== undefined) session.sessionDailyResetAt = updates.sessionDailyResetAt
265
+ if (updates.sessionResetTimezone !== undefined) session.sessionResetTimezone = updates.sessionResetTimezone
266
+ if (updates.thinkingLevel !== undefined) session.thinkingLevel = updates.thinkingLevel
267
+ if (updates.connectorThinkLevel !== undefined) session.connectorThinkLevel = updates.connectorThinkLevel
268
+ if (updates.connectorSessionScope !== undefined) session.connectorSessionScope = updates.connectorSessionScope
269
+ if (updates.connectorReplyMode !== undefined) session.connectorReplyMode = updates.connectorReplyMode
270
+ if (updates.connectorThreadBinding !== undefined) session.connectorThreadBinding = updates.connectorThreadBinding
271
+ if (updates.connectorGroupPolicy !== undefined) session.connectorGroupPolicy = updates.connectorGroupPolicy
272
+ if (updates.connectorIdleTimeoutSec !== undefined) session.connectorIdleTimeoutSec = updates.connectorIdleTimeoutSec
273
+ if (updates.connectorMaxAgeSec !== undefined) session.connectorMaxAgeSec = updates.connectorMaxAgeSec
274
+ if (updates.connectorContext !== undefined) session.connectorContext = updates.connectorContext
275
+ if (updates.identityState !== undefined) session.identityState = updates.identityState
276
+ if (updates.sessionArchiveState !== undefined) session.sessionArchiveState = updates.sessionArchiveState
277
+ if (updates.lastSessionResetAt !== undefined) session.lastSessionResetAt = updates.lastSessionResetAt
278
+ if (updates.lastSessionResetReason !== undefined) session.lastSessionResetReason = updates.lastSessionResetReason
279
+ if (updates.pinned !== undefined) session.pinned = !!updates.pinned
280
+ if (updates.claudeSessionId !== undefined) session.claudeSessionId = updates.claudeSessionId
281
+ if (updates.codexThreadId !== undefined) session.codexThreadId = updates.codexThreadId
282
+ if (updates.opencodeSessionId !== undefined) session.opencodeSessionId = updates.opencodeSessionId
283
+ if (updates.delegateResumeIds !== undefined) session.delegateResumeIds = updates.delegateResumeIds
284
+ if (!Array.isArray(session.messages)) session.messages = []
285
+
286
+ saveSession(sessionId, original)
287
+ notify('sessions')
288
+ return enrichSessionWithMissionSummary(original)
289
+ }
290
+
291
+ export function deleteChatSession(sessionId: string): boolean {
292
+ if (!getSession(sessionId)) return false
293
+ stopActiveSessionProcess(sessionId)
294
+ cleanupSessionProcesses(sessionId)
295
+ deleteSessionMessages(sessionId)
296
+ deleteSession(sessionId)
297
+ notify('sessions')
298
+ return true
299
+ }
300
+
301
+ export function getQueueSnapshot(sessionId: string) {
302
+ const session = getSession(sessionId)
303
+ if (!session) return null
304
+ return getSessionQueueSnapshot(sessionId)
305
+ }
306
+
307
+ export function queueChatMessage(sessionId: string, body: Record<string, unknown>): ServiceResult<Record<string, unknown>> {
308
+ const session = getSession(sessionId)
309
+ if (!session) return serviceFail(404, 'Not found')
310
+ const message = typeof body.message === 'string' ? body.message : ''
311
+ const imagePath = typeof body.imagePath === 'string' ? body.imagePath : undefined
312
+ const imageUrl = typeof body.imageUrl === 'string' ? body.imageUrl : undefined
313
+ const attachedFiles = Array.isArray(body.attachedFiles)
314
+ ? body.attachedFiles.filter((file): file is string => typeof file === 'string' && file.trim().length > 0)
315
+ : undefined
316
+ const replyToId = typeof body.replyToId === 'string' ? body.replyToId : undefined
317
+ const hasFiles = !!(imagePath || imageUrl || attachedFiles?.length)
318
+ if (!message.trim() && !hasFiles) {
319
+ return serviceFail(400, 'message or file is required')
320
+ }
321
+ const queued = enqueueSessionRun({
322
+ sessionId,
323
+ missionId: session.missionId || null,
324
+ message,
325
+ imagePath,
326
+ imageUrl,
327
+ attachedFiles,
328
+ source: 'chat',
329
+ mode: 'followup',
330
+ replyToId,
331
+ })
332
+ return serviceOk({
333
+ queued: {
334
+ runId: queued.runId,
335
+ position: queued.position,
336
+ },
337
+ snapshot: getSessionQueueSnapshot(sessionId),
338
+ })
339
+ }
340
+
341
+ export function cancelQueuedChatMessages(sessionId: string, runId?: string): ServiceResult<Record<string, unknown>> | null {
342
+ const session = getSession(sessionId)
343
+ if (!session) return null
344
+ const normalizedRunId = typeof runId === 'string' ? runId.trim() : ''
345
+ if (normalizedRunId) {
346
+ const snapshot = getSessionQueueSnapshot(sessionId)
347
+ if (!snapshot.items.some((item) => item.runId === normalizedRunId)) {
348
+ return serviceFail(404, 'Queued run not found')
349
+ }
350
+ cancelQueuedRunById(normalizedRunId, 'Removed from queue')
351
+ return serviceOk({ cancelled: 1, snapshot: getSessionQueueSnapshot(sessionId) })
352
+ }
353
+ const cancelled = cancelQueuedRunsForSession(sessionId, 'Cleared queued messages')
354
+ return serviceOk({ cancelled, snapshot: getSessionQueueSnapshot(sessionId) })
355
+ }
356
+
357
+ export function clearChatMessages(sessionId: string): boolean {
358
+ const session = getSession(sessionId)
359
+ if (!session) return false
360
+ clearMessages(sessionId)
361
+ session.messages = []
362
+ session.claudeSessionId = null
363
+ session.codexThreadId = null
364
+ session.opencodeSessionId = null
365
+ session.delegateResumeIds = emptyDelegateResumeIds()
366
+ saveSession(sessionId, session)
367
+ notify('sessions')
368
+ return true
369
+ }
370
+
371
+ export function retryChatTurn(sessionId: string): ServiceResult<{ message: string; imagePath: string | null }> {
372
+ const session = getSession(sessionId)
373
+ if (!session) return serviceFail(404, 'Session not found')
374
+ const msgs = getMessages(sessionId)
375
+ // Remove trailing assistant messages
376
+ while (msgs.length && msgs[msgs.length - 1].role === 'assistant') {
377
+ msgs.pop()
378
+ }
379
+ if (!msgs.length) {
380
+ clearMessages(sessionId)
381
+ return serviceOk({ message: '', imagePath: null })
382
+ }
383
+ const lastUser = msgs[msgs.length - 1]
384
+ const message = lastUser.text
385
+ const imagePath = lastUser.imagePath || null
386
+ msgs.pop()
387
+ // Truncate to the new length (keep seq 0..msgs.length-1)
388
+ if (msgs.length === 0) {
389
+ clearMessages(sessionId)
390
+ } else {
391
+ truncateAfter(sessionId, msgs.length - 1)
392
+ }
393
+ return serviceOk({ message, imagePath })
394
+ }
395
+
396
+ export function editAndResendChatTurn(sessionId: string, messageIndex: number, newText: string): ServiceResult<{ message: string }> {
397
+ const session = getSession(sessionId)
398
+ if (!session) return serviceFail(404, 'Not found')
399
+ const msgCount = getMessages(sessionId).length
400
+ if (typeof messageIndex !== 'number' || messageIndex < 0 || messageIndex >= msgCount) {
401
+ return serviceFail(400, 'Invalid message index')
402
+ }
403
+ // Keep messages up to but not including messageIndex
404
+ if (messageIndex === 0) {
405
+ clearMessages(sessionId)
406
+ } else {
407
+ truncateAfter(sessionId, messageIndex - 1)
408
+ }
409
+ return serviceOk({ message: newText })
410
+ }
@@ -5,7 +5,7 @@ import type {
5
5
  ConnectorAccessSnapshot,
6
6
  WhatsAppApprovedContact,
7
7
  } from '@/types'
8
- import { loadSettings } from '../storage'
8
+ import { loadSettings } from '../settings/settings-repository'
9
9
  import {
10
10
  createOrTouchPairingRequest,
11
11
  getSenderAddressingOverride,
@@ -1,7 +1,8 @@
1
1
  import { getProvider } from '@/lib/providers'
2
2
  import type { Connector } from '@/types'
3
- import { loadAgents } from '../storage'
3
+ import { loadAgents } from '@/lib/server/agents/agent-repository'
4
4
  import { syncSessionArchiveMemory } from '@/lib/server/memory/session-archive-memory'
5
+ import { getMessages, replaceAllMessages } from '@/lib/server/messages/message-repository'
5
6
  import { getEnabledCapabilityIds } from '@/lib/capability-selection'
6
7
  import { resolvePairingAccess } from './access'
7
8
  import {
@@ -189,9 +190,9 @@ export async function handleConnectorCommand(params: {
189
190
 
190
191
  if (command.name === 'status') {
191
192
  const policy = resolveConnectorSessionPolicy(connector, msg, session)
192
- const all = Array.isArray(session.messages) ? session.messages : []
193
- const userCount = all.filter((message: { role?: string }) => message?.role === 'user').length
194
- const assistantCount = all.filter((message: { role?: string }) => message?.role === 'assistant').length
193
+ const all = getMessages(session.id)
194
+ const userCount = all.filter((message) => message?.role === 'user').length
195
+ const assistantCount = all.filter((message) => message?.role === 'assistant').length
195
196
  const toolsCount = getEnabledCapabilityIds(session).length
196
197
  const statusText = [
197
198
  `Status for ${connector.platform} / ${connector.name}:`,
@@ -232,7 +233,7 @@ export async function handleConnectorCommand(params: {
232
233
  if (command.name === 'compact') {
233
234
  const keepParsed = Number.parseInt(command.args, 10)
234
235
  const keepLastN = Number.isFinite(keepParsed) ? Math.max(4, Math.min(50, keepParsed)) : 10
235
- const history = Array.isArray(session.messages) ? session.messages : []
236
+ const history = getMessages(session.id)
236
237
  if (history.length <= keepLastN) {
237
238
  const text = `Nothing to compact. Current history has ${history.length} message(s), keepLastN=${keepLastN}.`
238
239
  pushSessionMessage(session, 'user', inboundText)
@@ -249,7 +250,7 @@ export async function handleConnectorCommand(params: {
249
250
  time: Date.now(),
250
251
  kind: 'system' as const,
251
252
  }
252
- session.messages = [summaryMessage, ...recentMessages]
253
+ replaceAllMessages(session.id, [summaryMessage, ...recentMessages])
253
254
  session.lastActiveAt = Date.now()
254
255
  const text = `Compacted ${oldMessages.length} message(s). Kept ${recentMessages.length} recent message(s) plus a summary.`
255
256
  pushSessionMessage(session, 'assistant', text)