@swarmclawai/swarmclaw 1.2.4 → 1.2.6

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 (262) 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 +23 -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/providers/index.test.ts +108 -0
  109. package/src/lib/providers/index.ts +38 -15
  110. package/src/lib/query/client.ts +17 -0
  111. package/src/lib/server/agents/agent-runtime-config.ts +1 -1
  112. package/src/lib/server/agents/agent-service.ts +429 -0
  113. package/src/lib/server/agents/agent-thread-session.ts +6 -5
  114. package/src/lib/server/agents/autonomy-contract.ts +1 -4
  115. package/src/lib/server/agents/delegation-advisory.test.ts +206 -0
  116. package/src/lib/server/agents/delegation-advisory.ts +251 -0
  117. package/src/lib/server/agents/main-agent-loop.ts +98 -40
  118. package/src/lib/server/agents/subagent-runtime.ts +12 -0
  119. package/src/lib/server/autonomy/supervisor-reflection.test.ts +20 -1
  120. package/src/lib/server/autonomy/supervisor-reflection.ts +39 -19
  121. package/src/lib/server/build-llm.ts +7 -15
  122. package/src/lib/server/capability-router.test.ts +70 -1
  123. package/src/lib/server/capability-router.ts +24 -99
  124. package/src/lib/server/chat-execution/chat-execution-utils.ts +0 -15
  125. package/src/lib/server/chat-execution/chat-streaming-utils.ts +2 -4
  126. package/src/lib/server/chat-execution/chat-turn-finalization.ts +77 -12
  127. package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +4 -4
  128. package/src/lib/server/chat-execution/chat-turn-preflight.ts +2 -2
  129. package/src/lib/server/chat-execution/chat-turn-preparation.ts +41 -17
  130. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +4 -2
  131. package/src/lib/server/chat-execution/chat-turn-tool-routing.test.ts +45 -0
  132. package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +48 -17
  133. package/src/lib/server/chat-execution/continuation-evaluator.ts +4 -1
  134. package/src/lib/server/chat-execution/direct-memory-intent.test.ts +9 -0
  135. package/src/lib/server/chat-execution/direct-memory-intent.ts +12 -2
  136. package/src/lib/server/chat-execution/message-classifier.test.ts +35 -23
  137. package/src/lib/server/chat-execution/message-classifier.ts +74 -32
  138. package/src/lib/server/chat-execution/prompt-builder.test.ts +29 -0
  139. package/src/lib/server/chat-execution/prompt-builder.ts +37 -2
  140. package/src/lib/server/chat-execution/prompt-sections.test.ts +56 -0
  141. package/src/lib/server/chat-execution/prompt-sections.ts +193 -0
  142. package/src/lib/server/chat-execution/stream-agent-chat.ts +63 -7
  143. package/src/lib/server/chat-execution/stream-continuation.test.ts +36 -0
  144. package/src/lib/server/chat-execution/stream-continuation.ts +28 -13
  145. package/src/lib/server/chatrooms/chatroom-agent-signals.ts +26 -18
  146. package/src/lib/server/chatrooms/chatroom-helpers.ts +19 -18
  147. package/src/lib/server/chatrooms/chatroom-repository.ts +16 -0
  148. package/src/lib/server/chatrooms/chatroom-routing.test.ts +96 -0
  149. package/src/lib/server/chatrooms/chatroom-routing.ts +207 -53
  150. package/src/lib/server/chatrooms/mailbox-utils.ts +4 -2
  151. package/src/lib/server/chatrooms/session-mailbox.ts +50 -40
  152. package/src/lib/server/chats/chat-session-service.ts +410 -0
  153. package/src/lib/server/connectors/access.ts +1 -1
  154. package/src/lib/server/connectors/commands.ts +7 -6
  155. package/src/lib/server/connectors/connector-inbound.ts +14 -7
  156. package/src/lib/server/connectors/connector-outbound.ts +16 -11
  157. package/src/lib/server/connectors/connector-service.ts +453 -0
  158. package/src/lib/server/connectors/delivery.ts +17 -12
  159. package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -14
  160. package/src/lib/server/connectors/media.ts +1 -1
  161. package/src/lib/server/connectors/response-media.ts +1 -1
  162. package/src/lib/server/connectors/session-consolidation.ts +11 -7
  163. package/src/lib/server/connectors/session.ts +9 -7
  164. package/src/lib/server/connectors/voice-note.ts +2 -1
  165. package/src/lib/server/context-manager.ts +20 -1
  166. package/src/lib/server/cost.ts +2 -3
  167. package/src/lib/server/credentials/credential-repository.ts +43 -4
  168. package/src/lib/server/credentials/credential-service.ts +112 -0
  169. package/src/lib/server/daemon/admin-metadata.ts +64 -0
  170. package/src/lib/server/daemon/controller.ts +577 -0
  171. package/src/lib/server/daemon/daemon-runtime.ts +352 -0
  172. package/src/lib/server/daemon/daemon-status-repository.ts +63 -0
  173. package/src/lib/server/daemon/types.ts +101 -0
  174. package/src/lib/server/embeddings.ts +3 -9
  175. package/src/lib/server/eval/agent-regression.ts +3 -2
  176. package/src/lib/server/eval/runner.ts +2 -2
  177. package/src/lib/server/execution-brief.test.ts +167 -0
  178. package/src/lib/server/execution-brief.ts +295 -0
  179. package/src/lib/server/execution-engine/chat-turn.ts +9 -0
  180. package/src/lib/server/execution-engine/import-boundary.test.ts +44 -0
  181. package/src/lib/server/execution-engine/index.ts +35 -0
  182. package/src/lib/server/execution-engine/task-attempt.ts +303 -0
  183. package/src/lib/server/execution-engine/types.ts +33 -0
  184. package/src/lib/server/gateways/gateway-profile-repository.ts +47 -3
  185. package/src/lib/server/gateways/gateway-profile-service.ts +200 -0
  186. package/src/lib/server/memory/session-archive-memory.ts +12 -10
  187. package/src/lib/server/messages/message-repository.ts +330 -0
  188. package/src/lib/server/missions/mission-service/core.ts +8 -6
  189. package/src/lib/server/openclaw/agent-resolver.ts +2 -3
  190. package/src/lib/server/openclaw/doctor.ts +1 -1
  191. package/src/lib/server/openclaw/gateway.test.ts +10 -1
  192. package/src/lib/server/openclaw/gateway.ts +5 -14
  193. package/src/lib/server/openclaw/health.ts +3 -11
  194. package/src/lib/server/openclaw/sync.ts +8 -6
  195. package/src/lib/server/persistence/storage-context.ts +3 -0
  196. package/src/lib/server/protocols/protocol-agent-turn.ts +25 -17
  197. package/src/lib/server/protocols/protocol-normalization.ts +1 -1
  198. package/src/lib/server/protocols/protocol-queries.ts +13 -7
  199. package/src/lib/server/protocols/protocol-run-lifecycle.ts +16 -20
  200. package/src/lib/server/protocols/protocol-run-repository.ts +81 -0
  201. package/src/lib/server/protocols/protocol-step-processors.ts +23 -31
  202. package/src/lib/server/protocols/protocol-swarm.ts +8 -8
  203. package/src/lib/server/protocols/protocol-template-repository.ts +42 -0
  204. package/src/lib/server/protocols/protocol-templates.ts +4 -2
  205. package/src/lib/server/protocols/protocol-types.ts +10 -7
  206. package/src/lib/server/provider-endpoint.ts +7 -12
  207. package/src/lib/server/provider-model-discovery.ts +2 -11
  208. package/src/lib/server/query-expansion.ts +5 -6
  209. package/src/lib/server/run-context.test.ts +365 -0
  210. package/src/lib/server/run-context.ts +367 -0
  211. package/src/lib/server/runtime/heartbeat-service.ts +7 -5
  212. package/src/lib/server/runtime/queue/core.ts +61 -190
  213. package/src/lib/server/runtime/run-ledger.ts +8 -0
  214. package/src/lib/server/runtime/session-run-manager/drain.ts +2 -2
  215. package/src/lib/server/runtime/session-run-manager/enqueue.ts +6 -0
  216. package/src/lib/server/runtime/session-run-manager/state.ts +4 -0
  217. package/src/lib/server/schedules/schedule-route-service.ts +230 -0
  218. package/src/lib/server/service-result.ts +16 -0
  219. package/src/lib/server/session-note.ts +2 -3
  220. package/src/lib/server/session-reset-policy.ts +4 -3
  221. package/src/lib/server/session-tools/connector.ts +9 -6
  222. package/src/lib/server/session-tools/context-mgmt.ts +58 -9
  223. package/src/lib/server/session-tools/crud.ts +162 -10
  224. package/src/lib/server/session-tools/delegate.ts +1 -1
  225. package/src/lib/server/session-tools/manage-tasks.test.ts +152 -0
  226. package/src/lib/server/session-tools/memory.ts +6 -4
  227. package/src/lib/server/session-tools/session-info.test.ts +56 -0
  228. package/src/lib/server/session-tools/session-info.ts +119 -12
  229. package/src/lib/server/session-tools/skill-runtime.ts +3 -1
  230. package/src/lib/server/session-tools/skills.ts +15 -15
  231. package/src/lib/server/session-tools/subagent.test.ts +115 -1
  232. package/src/lib/server/session-tools/subagent.ts +125 -7
  233. package/src/lib/server/session-tools/team-context.ts +4 -3
  234. package/src/lib/server/session-tools/wallet.ts +0 -58
  235. package/src/lib/server/sessions/session-lineage.ts +55 -0
  236. package/src/lib/server/sessions/session-repository.ts +2 -2
  237. package/src/lib/server/skills/learned-skills.ts +24 -23
  238. package/src/lib/server/skills/runtime-skill-resolver.ts +2 -1
  239. package/src/lib/server/skills/skill-repository.ts +136 -13
  240. package/src/lib/server/skills/skill-suggestions.ts +25 -28
  241. package/src/lib/server/storage-normalization.test.ts +44 -267
  242. package/src/lib/server/storage-normalization.ts +75 -0
  243. package/src/lib/server/storage.ts +19 -0
  244. package/src/lib/server/structured-extract.ts +3 -14
  245. package/src/lib/server/tasks/task-followups.ts +16 -11
  246. package/src/lib/server/tasks/task-result.test.ts +25 -29
  247. package/src/lib/server/tasks/task-result.ts +5 -9
  248. package/src/lib/server/tasks/task-route-service.ts +449 -0
  249. package/src/lib/server/text-normalization.ts +41 -0
  250. package/src/lib/server/tool-planning.ts +6 -42
  251. package/src/lib/server/upload-path.ts +5 -0
  252. package/src/lib/server/working-state/extraction.ts +614 -0
  253. package/src/lib/server/working-state/normalization.ts +866 -0
  254. package/src/lib/server/working-state/prompt.ts +60 -0
  255. package/src/lib/server/working-state/repository.ts +38 -0
  256. package/src/lib/server/working-state/service.test.ts +253 -0
  257. package/src/lib/server/working-state/service.ts +293 -0
  258. package/src/lib/validation/schemas.ts +1 -0
  259. package/src/lib/ws-client.ts +3 -3
  260. package/src/stores/slices/task-slice.ts +1 -4
  261. package/src/stores/use-chatroom-store.ts +2 -2
  262. package/src/types/index.ts +277 -12
@@ -34,8 +34,16 @@ import { isHeartbeatSource } from '@/lib/server/runtime/heartbeat-source'
34
34
  import { perf } from '@/lib/server/runtime/perf'
35
35
  import { getAgent } from '@/lib/server/agents/agent-repository'
36
36
  import { isDirectConnectorSession } from '@/lib/server/connectors/session-kind'
37
- import { getSession, getSessionMessages, saveSession } from '@/lib/server/sessions/session-repository'
37
+ import { getSession, saveSession } from '@/lib/server/sessions/session-repository'
38
+ import {
39
+ getMessages,
40
+ getMessageCount,
41
+ appendMessage,
42
+ replaceMessageAt,
43
+ replaceAllMessages,
44
+ } from '@/lib/server/messages/message-repository'
38
45
  import { appendUsage } from '@/lib/server/usage/usage-repository'
46
+ import { synchronizeWorkingStateForTurn } from '@/lib/server/working-state/service'
39
47
  import { notify } from '@/lib/server/ws-hub'
40
48
 
41
49
  import type { ExecuteChatTurnInput, ExecuteChatTurnResult } from './chat-execution-types'
@@ -193,7 +201,7 @@ export async function finalizeChatTurn(params: {
193
201
  const totalTokens = inputTokens + outputTokens
194
202
  if (totalTokens > 0) {
195
203
  const cost = estimateCost(sessionForRun.model, inputTokens, outputTokens)
196
- const history = getSessionMessages(sessionId)
204
+ const history = getMessages(sessionId)
197
205
  const usageRecord: UsageRecord = {
198
206
  sessionId,
199
207
  messageIndex: history.length,
@@ -354,15 +362,17 @@ export async function finalizeChatTurn(params: {
354
362
  const current = getSession(sessionId)
355
363
  let assistantPersisted = false
356
364
  if (current) {
357
- current.messages = Array.isArray(current.messages) ? current.messages : []
365
+ // Load messages from relational table (lazy-migrates from blob on first access)
366
+ const messages = getMessages(sessionId)
367
+ let messagesPruned = false
358
368
  if (!isDirectConnectorSession(current) && current.connectorContext) {
359
369
  current.connectorContext = undefined
360
370
  }
361
371
  const currentAgent = current.agentId ? getAgent(current.agentId) : null
362
- pruneStreamingAssistantArtifacts(current.messages, {
372
+ if (pruneStreamingAssistantArtifacts(messages, {
363
373
  minIndex: runMessageStartIndex,
364
374
  minTime: runStartedAt,
365
- })
375
+ })) messagesPruned = true
366
376
  const persistField = (key: string, value: unknown) => {
367
377
  const normalized = normalizeResumeId(value)
368
378
  if ((current as unknown as Record<string, unknown>)[key] !== normalized) {
@@ -392,6 +402,8 @@ export async function finalizeChatTurn(params: {
392
402
  }
393
403
  }
394
404
 
405
+ let persistedAssistantMsg: Message | null = null
406
+ let replacedLast = false
395
407
  if (shouldPersistAssistant) {
396
408
  const persistedKind = isHeartbeatRun ? 'heartbeat' : 'chat'
397
409
  const nowTs = Date.now()
@@ -410,7 +422,7 @@ export async function finalizeChatTurn(params: {
410
422
  runId: lifecycleRunId,
411
423
  })
412
424
  if (nextAssistantMessage) {
413
- const previous = current.messages.at(-1)
425
+ const previous = messages.at(-1)
414
426
  const nextToolEvents = nextAssistantMessage.toolEvents || []
415
427
  const nextKind = nextAssistantMessage.kind || persistedKind
416
428
  if (shouldSuppressRedundantConnectorDeliveryFollowup({
@@ -433,11 +445,14 @@ export async function finalizeChatTurn(params: {
433
445
  nextKind,
434
446
  now: nowTs,
435
447
  })) {
436
- current.messages[current.messages.length - 1] = nextAssistantMessage
448
+ messages[messages.length - 1] = nextAssistantMessage
437
449
  assistantPersisted = true
450
+ replacedLast = true
451
+ persistedAssistantMsg = nextAssistantMessage
438
452
  } else {
439
- current.messages.push(nextAssistantMessage)
453
+ messages.push(nextAssistantMessage)
440
454
  assistantPersisted = true
455
+ persistedAssistantMsg = nextAssistantMessage
441
456
  }
442
457
  persistedResponseForHooks = nextAssistantMessage.text
443
458
  if (assistantPersisted) {
@@ -534,16 +549,28 @@ export async function finalizeChatTurn(params: {
534
549
  }
535
550
  }
536
551
  if (isHeartbeatRun && heartbeatClassification === 'suppress') {
537
- pruneSuppressedHeartbeatStreamMessage(current.messages)
552
+ if (pruneSuppressedHeartbeatStreamMessage(messages)) messagesPruned = true
538
553
  }
539
554
 
540
555
  if (isHeartbeatRun) {
541
- const pruned = pruneOldHeartbeatMessages(current.messages)
556
+ const pruned = pruneOldHeartbeatMessages(messages)
542
557
  if (pruned > 0) {
558
+ messagesPruned = true
543
559
  log.info('heartbeat', `Pruned ${pruned} old heartbeat message(s) from session ${sessionId}`)
544
560
  }
545
561
  }
546
562
 
563
+ // Persist messages: use O(1) append/replace when no pruning, O(n) replaceAll when pruned
564
+ if (messagesPruned) {
565
+ replaceAllMessages(sessionId, messages)
566
+ } else if (assistantPersisted && persistedAssistantMsg) {
567
+ if (replacedLast) {
568
+ replaceMessageAt(sessionId, getMessageCount(sessionId) - 1, persistedAssistantMsg)
569
+ } else {
570
+ appendMessage(sessionId, persistedAssistantMsg)
571
+ }
572
+ }
573
+
547
574
  try {
548
575
  await runCapabilityHook('afterChatTurn', {
549
576
  session: current,
@@ -563,8 +590,9 @@ export async function finalizeChatTurn(params: {
563
590
 
564
591
  refreshSessionIdentityState(current, currentAgent)
565
592
  let resolvedMissionId = mission?.id || current.missionId || null
593
+ let updatedMission = mission || null
566
594
  if (resolvedMissionId) {
567
- const updatedMission = await applyMissionOutcomeForTurn({
595
+ updatedMission = await applyMissionOutcomeForTurn({
568
596
  session: current,
569
597
  missionId: resolvedMissionId,
570
598
  source,
@@ -579,6 +607,43 @@ export async function finalizeChatTurn(params: {
579
607
  current.missionId = updatedMission.id
580
608
  }
581
609
  }
610
+ const missionStateChanged = Boolean(
611
+ updatedMission
612
+ && (
613
+ updatedMission.id !== mission?.id
614
+ || updatedMission.updatedAt !== mission?.updatedAt
615
+ || updatedMission.status !== mission?.status
616
+ || updatedMission.phase !== mission?.phase
617
+ || updatedMission.currentStep !== mission?.currentStep
618
+ || updatedMission.waitState?.reason !== mission?.waitState?.reason
619
+ )
620
+ )
621
+ const shouldSyncWorkingState = (
622
+ (!isHeartbeatRun && (assistantPersisted || persistedToolEvents.length > 0 || Boolean(errorMessage)))
623
+ || (isHeartbeatRun && (persistedToolEvents.length > 0 || Boolean(errorMessage) || missionStateChanged))
624
+ )
625
+ if (shouldSyncWorkingState) {
626
+ try {
627
+ await synchronizeWorkingStateForTurn({
628
+ sessionId,
629
+ agentId: current.agentId || null,
630
+ mission: updatedMission,
631
+ message,
632
+ assistantText: hiddenControlOnly ? '' : textForPersistence,
633
+ error: errorMessage || null,
634
+ toolEvents: persistedToolEvents,
635
+ runId: lifecycleRunId,
636
+ source,
637
+ })
638
+ } catch (workingStateError: unknown) {
639
+ log.warn('chat-run', `Working-state sync failed for session ${sessionId}`, {
640
+ runId: lifecycleRunId,
641
+ error: typeof workingStateError === 'object' && workingStateError !== null && 'message' in workingStateError
642
+ ? (workingStateError as Error).message
643
+ : String(workingStateError),
644
+ })
645
+ }
646
+ }
582
647
  try {
583
648
  syncSessionArchiveMemory(current, { agent: currentAgent })
584
649
  } catch {
@@ -591,7 +656,7 @@ export async function finalizeChatTurn(params: {
591
656
  isHeartbeatRun,
592
657
  agentAutoDraftSetting: currentAgent?.autoDraftSkillSuggestions === true,
593
658
  toolEventCount: persistedToolEvents.length,
594
- messageCount: current.messages.length,
659
+ messageCount: messages.length,
595
660
  })) {
596
661
  try {
597
662
  const { createSkillSuggestionFromSession } = await import('@/lib/server/skills/skill-suggestions')
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getSession,
3
- saveSession,
4
3
  } from '@/lib/server/sessions/session-repository'
4
+ import { getMessages, replaceAllMessages } from '@/lib/server/messages/message-repository'
5
5
  import { notify } from '@/lib/server/ws-hub'
6
6
  import type { MessageToolEvent, SSEEvent } from '@/types'
7
7
  import { upsertStreamingAssistantArtifact } from '@/lib/chat/chat-streaming-state'
@@ -72,7 +72,7 @@ export function createPartialAssistantPersistence(input: {
72
72
  try {
73
73
  const current = getSession(prepared.sessionId)
74
74
  if (!current) return
75
- current.messages = Array.isArray(current.messages) ? current.messages : []
75
+ const currentMessages = getMessages(prepared.sessionId)
76
76
  const partialMsg = await applyMessageLifecycleHooks({
77
77
  session: current,
78
78
  message: {
@@ -98,11 +98,11 @@ export function createPartialAssistantPersistence(input: {
98
98
  if (snapshotKey === lastPartialSnapshotKey) return
99
99
  lastPartialSnapshotKey = snapshotKey
100
100
  lastPartialSaveAt = Date.now()
101
- upsertStreamingAssistantArtifact(current.messages, partialMsg, {
101
+ upsertStreamingAssistantArtifact(currentMessages, partialMsg, {
102
102
  minIndex: prepared.runMessageStartIndex,
103
103
  minTime: prepared.runStartedAt,
104
104
  })
105
- saveSession(prepared.sessionId, current)
105
+ replaceAllMessages(prepared.sessionId, currentMessages)
106
106
  notify(`messages:${prepared.sessionId}`)
107
107
  } catch {
108
108
  // Partial persistence is best-effort.
@@ -1,5 +1,6 @@
1
1
  import { notify } from '@/lib/server/ws-hub'
2
2
  import { getSession, saveSession } from '@/lib/server/sessions/session-repository'
3
+ import { appendMessage } from '@/lib/server/messages/message-repository'
3
4
  import type { MessageToolEvent, SSEEvent } from '@/types'
4
5
  import type { ExecuteChatTurnResult } from './chat-execution-types'
5
6
  import {
@@ -47,8 +48,7 @@ async function completeSyntheticAssistantTurn(params: {
47
48
  isSynthetic: true,
48
49
  })
49
50
  if (nextAssistantMessage) {
50
- session.messages = Array.isArray(session.messages) ? session.messages : []
51
- session.messages.push(nextAssistantMessage)
51
+ appendMessage(params.sessionId, nextAssistantMessage)
52
52
  session.lastActiveAt = Date.now()
53
53
  saveSession(params.sessionId, session)
54
54
  if (params.notifyMessages) notify(`messages:${params.sessionId}`)
@@ -2,13 +2,14 @@ import fs from 'fs'
2
2
  import os from 'os'
3
3
 
4
4
  import { getProvider } from '@/lib/providers'
5
- import type { Message, Session } from '@/types'
5
+ import type { ExecutionBrief, Message, Session } from '@/types'
6
6
  import {
7
7
  decryptKey,
8
8
  loadCredentials,
9
9
  } from '@/lib/server/credentials/credential-repository'
10
10
  import { getAgent } from '@/lib/server/agents/agent-repository'
11
11
  import { getSession, saveSession } from '@/lib/server/sessions/session-repository'
12
+ import { getMessages, getMessageCount, appendMessage } from '@/lib/server/messages/message-repository'
12
13
  import { loadSettings } from '@/lib/server/settings/settings-repository'
13
14
  import { loadSkills } from '@/lib/server/skills/skill-repository'
14
15
  import { resolveImagePath } from '@/lib/server/resolve-image'
@@ -45,7 +46,6 @@ import {
45
46
  import { normalizeProviderEndpoint, isLocalOpenClawEndpoint } from '@/lib/openclaw/openclaw-endpoint'
46
47
  import { NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
47
48
  import {
48
- buildMissionContextBlock,
49
49
  resolveMissionForTurn,
50
50
  } from '@/lib/server/missions/mission-service'
51
51
  import {
@@ -69,7 +69,15 @@ import {
69
69
  resetSessionRuntime,
70
70
  resolveSessionResetPolicy,
71
71
  } from '@/lib/server/session-reset-policy'
72
+ import {
73
+ buildExecutionBrief,
74
+ buildExecutionBriefContextBlock,
75
+ } from '@/lib/server/execution-brief'
72
76
  import { checkAgentBudgetLimits } from '@/lib/server/cost'
77
+ import {
78
+ classifyMessage,
79
+ toMessageSemanticsSummary,
80
+ } from '@/lib/server/chat-execution/message-classifier'
73
81
  import {
74
82
  filterRuntimeCapabilityIds,
75
83
  getTodaySpendUsd,
@@ -408,13 +416,13 @@ function resolveApiKeyForSession(session: SessionWithCredentials, provider: Prov
408
416
  if (!session.credentialId) throw new Error('No API key configured for this session')
409
417
  const creds = loadCredentials()
410
418
  const cred = creds[session.credentialId]
411
- if (!cred) throw new Error('API key not found. Please add one in Settings.')
419
+ if (!cred?.encryptedKey) throw new Error('API key not found. Please add one in Settings.')
412
420
  return decryptKey(cred.encryptedKey)
413
421
  }
414
422
  if (provider.optionalApiKey && session.credentialId) {
415
423
  const creds = loadCredentials()
416
424
  const cred = creds[session.credentialId]
417
- if (cred) {
425
+ if (cred?.encryptedKey) {
418
426
  try { return decryptKey(cred.encryptedKey) } catch { return null }
419
427
  }
420
428
  }
@@ -445,7 +453,8 @@ export interface PreparedExecutableChatTurn {
445
453
  lifecycleRunId: string
446
454
  agentForSession: ReturnType<typeof getAgent>
447
455
  mission: Awaited<ReturnType<typeof resolveMissionForTurn>>
448
- missionContextBlock?: string
456
+ executionBrief: ExecutionBrief
457
+ executionBriefContextBlock?: string
449
458
  extensionsForRun: string[]
450
459
  effectiveMessage: string
451
460
  providerType: string
@@ -492,9 +501,8 @@ export async function prepareChatTurn(input: ExecuteChatTurnInput): Promise<Prep
492
501
 
493
502
  const session = getSession(sessionId)
494
503
  if (!session) throw new Error(`Session not found: ${sessionId}`)
495
- session.messages = Array.isArray(session.messages) ? session.messages : []
496
504
  const runStartedAt = Date.now()
497
- const runMessageStartIndex = session.messages.length
505
+ const runMessageStartIndex = getMessageCount(sessionId)
498
506
 
499
507
  const appSettings = loadSettings()
500
508
  const lifecycleRunId = runId || `${sessionId}:${runStartedAt}`
@@ -542,7 +550,7 @@ export async function prepareChatTurn(input: ExecuteChatTurnInput): Promise<Prep
542
550
  {
543
551
  sessionId: session.id,
544
552
  session,
545
- messageCount: Array.isArray(session.messages) ? session.messages.length : 0,
553
+ messageCount: getMessageCount(sessionId),
546
554
  durationMs: Date.now() - (session.createdAt || runStartedAt),
547
555
  reason: freshness.reason || 'session_reset',
548
556
  },
@@ -615,12 +623,16 @@ export async function prepareChatTurn(input: ExecuteChatTurnInput): Promise<Prep
615
623
  if (isHeartbeatRun && input.modelOverride) {
616
624
  sessionForRun = { ...sessionForRun, model: input.modelOverride }
617
625
  }
618
- const missionContextBlock = buildMissionContextBlock(mission)
626
+ const executionBrief = buildExecutionBrief({
627
+ session: sessionForRun,
628
+ mission,
629
+ })
630
+ const executionBriefContextBlock = buildExecutionBriefContextBlock(executionBrief)
619
631
 
620
632
  if (extensionsForRun.length > 0) {
621
633
  const modelResolvePrompt = heartbeatLightContext
622
- ? (joinSystemPromptBlocks(buildLightHeartbeatSystemPrompt(sessionForRun), missionContextBlock) || '')
623
- : (joinSystemPromptBlocks(buildAgentSystemPrompt(sessionForRun), missionContextBlock) || '')
634
+ ? (joinSystemPromptBlocks(buildLightHeartbeatSystemPrompt(sessionForRun), executionBriefContextBlock) || '')
635
+ : (joinSystemPromptBlocks(buildAgentSystemPrompt(sessionForRun), executionBriefContextBlock) || '')
624
636
  const modelResolve = await runCapabilityBeforeModelResolve(
625
637
  {
626
638
  session: sessionForRun,
@@ -710,7 +722,17 @@ export async function prepareChatTurn(input: ExecuteChatTurnInput): Promise<Prep
710
722
 
711
723
  const shouldPersistUserMessage = shouldPersistInboundUserMessage(internal, source)
712
724
  if (shouldPersistUserMessage) {
713
- const linkAnalysis = !internal ? await runLinkUnderstanding(message) : []
725
+ const [linkAnalysis, semantics] = await Promise.all([
726
+ !internal ? runLinkUnderstanding(message) : Promise.resolve([]),
727
+ classifyMessage({
728
+ sessionId,
729
+ agentId: session.agentId || null,
730
+ message,
731
+ history: getMessages(sessionId),
732
+ })
733
+ .then((classification) => toMessageSemanticsSummary(classification))
734
+ .catch(() => undefined),
735
+ ])
714
736
  const guardedUserText = guardUntrustedText({
715
737
  text: message,
716
738
  source,
@@ -727,13 +749,14 @@ export async function prepareChatTurn(input: ExecuteChatTurnInput): Promise<Prep
727
749
  imageUrl: imageUrl || undefined,
728
750
  attachedFiles: attachedFiles?.length ? attachedFiles : undefined,
729
751
  replyToId: input.replyToId || undefined,
752
+ ...(semantics ? { semantics } : {}),
730
753
  },
731
754
  enabledIds: extensionsForRun,
732
755
  phase: 'user',
733
756
  runId: lifecycleRunId,
734
757
  })
735
758
  if (nextUserMessage) {
736
- session.messages.push(nextUserMessage)
759
+ appendMessage(sessionId, nextUserMessage)
737
760
  if (linkAnalysis.length > 0) {
738
761
  const linkAnalysisMessage = await applyMessageLifecycleHooks({
739
762
  session,
@@ -749,7 +772,7 @@ export async function prepareChatTurn(input: ExecuteChatTurnInput): Promise<Prep
749
772
  isSynthetic: true,
750
773
  })
751
774
  if (linkAnalysisMessage) {
752
- session.messages.push(linkAnalysisMessage)
775
+ appendMessage(sessionId, linkAnalysisMessage)
753
776
  }
754
777
  }
755
778
  session.lastActiveAt = Date.now()
@@ -781,8 +804,8 @@ export async function prepareChatTurn(input: ExecuteChatTurnInput): Promise<Prep
781
804
  && !useLocalOpenClawNativeRuntime
782
805
 
783
806
  const systemPrompt = heartbeatLightContext
784
- ? joinSystemPromptBlocks(buildLightHeartbeatSystemPrompt(sessionForRun), missionContextBlock)
785
- : (hasExtensions ? undefined : joinSystemPromptBlocks(buildAgentSystemPrompt(sessionForRun), missionContextBlock))
807
+ ? joinSystemPromptBlocks(buildLightHeartbeatSystemPrompt(sessionForRun), executionBriefContextBlock)
808
+ : (hasExtensions ? undefined : joinSystemPromptBlocks(buildAgentSystemPrompt(sessionForRun), executionBriefContextBlock))
786
809
 
787
810
  return {
788
811
  kind: 'ready',
@@ -797,7 +820,8 @@ export async function prepareChatTurn(input: ExecuteChatTurnInput): Promise<Prep
797
820
  lifecycleRunId,
798
821
  agentForSession,
799
822
  mission,
800
- missionContextBlock: missionContextBlock || undefined,
823
+ executionBrief,
824
+ executionBriefContextBlock: executionBriefContextBlock || undefined,
801
825
  extensionsForRun,
802
826
  effectiveMessage,
803
827
  providerType,
@@ -74,7 +74,8 @@ export async function executePreparedChatTurn(params: {
74
74
  resolvedImagePath,
75
75
  heartbeatLightContext,
76
76
  isAutoRunNoHistory,
77
- missionContextBlock,
77
+ executionBrief,
78
+ executionBriefContextBlock,
78
79
  } = prepared
79
80
 
80
81
  const emit = partialPersistence.emit
@@ -144,7 +145,8 @@ export async function executePreparedChatTurn(params: {
144
145
  attachedFiles,
145
146
  apiKey,
146
147
  systemPrompt,
147
- extraSystemContext: missionContextBlock ? [missionContextBlock] : undefined,
148
+ executionBrief,
149
+ extraSystemContext: [executionBriefContextBlock].filter((value): value is string => typeof value === 'string' && value.trim().length > 0),
148
150
  write: (raw) => parseAndEmit(raw),
149
151
  history: heartbeatHistory ?? applyContextClearBoundary(getSessionMessages(sessionId)),
150
152
  signal: abortController.signal,
@@ -439,6 +439,51 @@ describe('chat-turn-tool-routing', () => {
439
439
  assert.equal(result.calledNames.has('memory_search'), true)
440
440
  })
441
441
 
442
+ it('uses classifier-backed memory list fallback for broad memory inventory requests', async () => {
443
+ const invocations: Array<{ toolName: string; args: Record<string, unknown> }> = []
444
+ const result = await runPostLlmToolRouting({
445
+ session: {
446
+ cwd: process.cwd(),
447
+ tools: ['memory'],
448
+ },
449
+ sessionId: 'session-memory-list',
450
+ message: 'List everything you remember about me.',
451
+ effectiveMessage: 'List everything you remember about me.',
452
+ enabledExtensions: ['memory'],
453
+ toolPolicy: resolveSessionToolPolicy(['memory'], {}),
454
+ appSettings: {},
455
+ internal: false,
456
+ source: 'chat',
457
+ toolEvents: [],
458
+ emit: () => {},
459
+ }, '', undefined, {
460
+ classifyDirectMemoryIntent: async () => ({
461
+ action: 'list',
462
+ confidence: 0.91,
463
+ }),
464
+ invokeTool: async (_ctx, toolName, args, _failurePrefix, calledNames) => {
465
+ invocations.push({ toolName, args })
466
+ calledNames.add(toolName)
467
+ return {
468
+ invoked: true,
469
+ responseOverride: null,
470
+ toolOutputText: '[mem_1] favorite editor: Neovim\n[mem_2] timezone: Europe/Isle_of_Man',
471
+ }
472
+ },
473
+ })
474
+
475
+ assert.equal(invocations.length, 1)
476
+ assert.equal(invocations[0].toolName, 'memory_tool')
477
+ assert.deepEqual(invocations[0].args, {
478
+ action: 'list',
479
+ key: '',
480
+ scope: 'auto',
481
+ })
482
+ assert.equal(result.fullResponse, '[mem_1] favorite editor: Neovim\n[mem_2] timezone: Europe/Isle_of_Man')
483
+ assert.equal(result.errorMessage, undefined)
484
+ assert.equal(result.calledNames.has('memory_tool'), true)
485
+ })
486
+
442
487
  it('fails open when post-LLM memory classification times out', async () => {
443
488
  let invoked = false
444
489
  const started = Date.now()
@@ -22,6 +22,7 @@ import { log } from '@/lib/server/logger'
22
22
  import { rankDelegatesByHealth } from '@/lib/server/provider-health'
23
23
  import { routeTaskIntent, type CapabilityRoutingDecision } from '@/lib/server/capability-router'
24
24
  import { canonicalizeExtensionId, extensionIdMatches } from '@/lib/server/tool-aliases'
25
+ import { classifyMessage, type MessageClassification } from '@/lib/server/chat-execution/message-classifier'
25
26
  import {
26
27
  buildDirectMemoryRecallResponse,
27
28
  classifyDirectMemoryIntent,
@@ -37,7 +38,6 @@ import {
37
38
  findFirstUrl,
38
39
  hasToolEnabled,
39
40
  hasDirectLocalCodingTools,
40
- isMemoryListIntent,
41
41
  requestedToolNamesFromMessage,
42
42
  translateRequestedToolInvocation,
43
43
  } from '@/lib/server/chat-execution/chat-execution-utils'
@@ -61,6 +61,7 @@ export interface ToolRoutingContext {
61
61
  source: string
62
62
  toolEvents: MessageToolEvent[]
63
63
  emit: (ev: SSEEvent) => void
64
+ classification?: MessageClassification | null
64
65
  }
65
66
 
66
67
  export interface ToolRoutingResult {
@@ -126,6 +127,16 @@ async function resolveDirectMemoryIntentWithTimeout(
126
127
  }
127
128
  }
128
129
 
130
+ async function resolveTurnClassification(ctx: ToolRoutingContext): Promise<MessageClassification | null> {
131
+ if (ctx.classification !== undefined) return ctx.classification ?? null
132
+ if (ctx.internal || ctx.source !== 'chat') return null
133
+ return classifyMessage({
134
+ sessionId: ctx.sessionId,
135
+ agentId: ctx.session.agentId || null,
136
+ message: ctx.message,
137
+ }).catch(() => null)
138
+ }
139
+
129
140
  export async function runExclusiveDirectMemoryPreflight(
130
141
  ctx: ToolRoutingContext,
131
142
  hooks?: ToolRoutingHooks,
@@ -148,14 +159,18 @@ export async function runExclusiveDirectMemoryPreflight(
148
159
  ? 'memory_store'
149
160
  : directMemoryIntent.action === 'update'
150
161
  ? 'memory_update'
151
- : 'memory_search'
162
+ : directMemoryIntent.action === 'list'
163
+ ? 'memory_tool'
164
+ : 'memory_search'
152
165
 
153
166
  const args: Record<string, unknown> = directMemoryIntent.action === 'recall'
154
167
  ? { query: directMemoryIntent.query, scope: 'auto' }
155
- : {
156
- value: directMemoryIntent.value,
157
- ...(directMemoryIntent.title ? { title: directMemoryIntent.title } : {}),
158
- }
168
+ : directMemoryIntent.action === 'list'
169
+ ? { action: 'list', key: '', scope: 'auto' }
170
+ : {
171
+ value: directMemoryIntent.value,
172
+ ...(directMemoryIntent.title ? { title: directMemoryIntent.title } : {}),
173
+ }
159
174
 
160
175
  const result = await invokeTool(
161
176
  ctx,
@@ -192,6 +207,15 @@ export async function runExclusiveDirectMemoryPreflight(
192
207
  }
193
208
  }
194
209
 
210
+ if (directMemoryIntent.action === 'list') {
211
+ return {
212
+ calledNames,
213
+ fullResponse: String(result.toolOutputText || '').trim() || 'No memories found.',
214
+ errorMessage: undefined,
215
+ missedRequestedTools: [],
216
+ }
217
+ }
218
+
195
219
  if (directMemoryIntent.action === 'recall') {
196
220
  const recallResponse = result.toolOutputText
197
221
  ? buildDirectMemoryRecallResponse(directMemoryIntent, result.toolOutputText)
@@ -494,12 +518,13 @@ export async function runPostLlmToolRouting(
494
518
  const unavailableRequestedTools = new Map<string, string>()
495
519
  let fullResponse = currentResponse
496
520
  let errorMessage = currentError
521
+ const classification = await resolveTurnClassification(ctx)
497
522
 
498
523
  const requestedToolNames = (!ctx.internal && ctx.source === 'chat')
499
524
  ? requestedToolNamesFromMessage(ctx.message)
500
525
  : []
501
526
  const routingDecision: CapabilityRoutingDecision | null = (!ctx.internal && ctx.source === 'chat')
502
- ? routeTaskIntent(ctx.message, ctx.enabledExtensions, ctx.appSettings)
527
+ ? routeTaskIntent(ctx.message, ctx.enabledExtensions, ctx.appSettings, classification)
503
528
  : null
504
529
 
505
530
  // --- Forced connector_message_tool ---
@@ -609,6 +634,22 @@ export async function runPostLlmToolRouting(
609
634
  }
610
635
  }
611
636
 
637
+ if (directMemoryIntent?.action === 'list') {
638
+ const result = await invokeTool(
639
+ ctx,
640
+ 'memory_tool',
641
+ { action: 'list', key: '', scope: 'auto' },
642
+ 'Forced memory list invocation failed',
643
+ calledNames,
644
+ )
645
+ if (result.blockedReason) policyBlockedTools.set('memory_tool', result.blockedReason)
646
+ if (result.unavailableReason) unavailableRequestedTools.set('memory_tool', result.unavailableReason)
647
+ if (result.invoked) {
648
+ fullResponse = String(result.toolOutputText || '').trim() || 'No memories found.'
649
+ errorMessage = undefined
650
+ }
651
+ }
652
+
612
653
  // --- Auto-delegation for coding intent ---
613
654
  const hasDelegationCall = FORCED_DELEGATION_TOOLS.some((t) => calledNames.has(t))
614
655
  const enabledDelegates = enabledDelegationTools(ctx.session)
@@ -690,16 +731,6 @@ export async function runPostLlmToolRouting(
690
731
  }
691
732
  }
692
733
 
693
- if (canAutoRoute && calledNames.size === 0 && hasToolEnabled(ctx.session, 'memory') && isMemoryListIntent(ctx.message)) {
694
- const result = await invokeTool(
695
- ctx, 'memory_tool',
696
- { action: 'list', key: '', scope: 'auto' },
697
- 'Auto memory listing failed',
698
- calledNames,
699
- )
700
- if (result.responseOverride) fullResponse = result.responseOverride
701
- }
702
-
703
734
  const explicitArtifactTarget = extractExplicitArtifactTarget(ctx.message)
704
735
  const canAutoSaveArtifact = (!ctx.internal && ctx.source === 'chat')
705
736
  && !!explicitArtifactTarget
@@ -42,6 +42,8 @@ export interface ContinuationContext {
42
42
  sessionExtensions: string[]
43
43
  isConnectorSession: boolean
44
44
  isCoordinatorAgent: boolean
45
+ delegationEnabled: boolean
46
+ delegationPreferenceActive: boolean
45
47
  history: Message[]
46
48
  session: { cwd: string }
47
49
  write: (data: string) => void
@@ -279,6 +281,7 @@ function checkAttachmentFollowthrough(ctx: ContinuationContext): ContinuationDec
279
281
  enabledExtensions: ctx.sessionExtensions,
280
282
  hasToolCalls: ctx.state.hasToolCalls,
281
283
  hasAttachmentContext: ctx.hasAttachmentContext,
284
+ classification: ctx.classification,
282
285
  })) return null
283
286
  const count = ctx.limits.increment('attachment_followthrough')
284
287
  const { max } = ctx.limits.getStatus('attachment_followthrough')
@@ -335,7 +338,7 @@ function checkToolErrorFollowthrough(ctx: ContinuationContext): ContinuationDeci
335
338
  }
336
339
 
337
340
  function checkCoordinatorDelegation(ctx: ContinuationContext): ContinuationDecision | null {
338
- if (!ctx.isCoordinatorAgent) return null
341
+ if (!ctx.delegationEnabled || !ctx.delegationPreferenceActive) return null
339
342
  if (!ctx.limits.canContinue('coordinator_delegation_nudge')) return null
340
343
  // Skip if already delegated
341
344
  const delegationTools = ['spawn_subagent', 'manage_protocols']
@@ -46,6 +46,15 @@ describe('direct-memory-intent', () => {
46
46
  })
47
47
  })
48
48
 
49
+ it('parses a list classification payload', () => {
50
+ const parsed = parseDirectMemoryIntentResponse('{"action":"list","confidence":0.84}')
51
+
52
+ assert.deepEqual(parsed, {
53
+ action: 'list',
54
+ confidence: 0.84,
55
+ })
56
+ })
57
+
49
58
  it('parses exclusive completion for pure memory turns', () => {
50
59
  const parsed = parseDirectMemoryIntentResponse('{"action":"store","confidence":0.98,"title":"Launch marker","value":"My launch marker is ALPHA-9","acknowledgement":"I\\u2019ll remember that.","exclusiveCompletion":true}')
51
60