@swarmclawai/swarmclaw 1.2.0 → 1.2.2

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 (241) hide show
  1. package/README.md +19 -0
  2. package/package.json +5 -2
  3. package/skills/coding-agent/SKILL.md +111 -0
  4. package/skills/github/SKILL.md +140 -0
  5. package/skills/nano-banana-pro/SKILL.md +62 -0
  6. package/skills/nano-banana-pro/scripts/generate_image.py +235 -0
  7. package/skills/nano-pdf/SKILL.md +53 -0
  8. package/skills/openai-image-gen/SKILL.md +78 -0
  9. package/skills/openai-image-gen/scripts/gen.py +328 -0
  10. package/skills/resourceful-problem-solving/SKILL.md +49 -0
  11. package/skills/skill-creator/SKILL.md +147 -0
  12. package/skills/skill-creator/scripts/init_skill.py +378 -0
  13. package/skills/skill-creator/scripts/quick_validate.py +159 -0
  14. package/skills/summarize/SKILL.md +77 -0
  15. package/src/app/api/auth/route.ts +20 -5
  16. package/src/app/api/chats/[id]/deploy/route.ts +11 -6
  17. package/src/app/api/chats/[id]/devserver/route.ts +17 -20
  18. package/src/app/api/chats/[id]/messages/route.ts +15 -11
  19. package/src/app/api/chats/[id]/route.ts +9 -10
  20. package/src/app/api/chats/[id]/stop/route.ts +5 -7
  21. package/src/app/api/chats/messages-route.test.ts +8 -6
  22. package/src/app/api/chats/route.ts +9 -10
  23. package/src/app/api/credentials/[id]/route.ts +4 -1
  24. package/src/app/api/extensions/marketplace/route.ts +5 -2
  25. package/src/app/api/ip/route.ts +2 -2
  26. package/src/app/api/memory/maintenance/route.ts +5 -2
  27. package/src/app/api/preview-server/route.ts +15 -12
  28. package/src/app/api/projects/[id]/route.ts +7 -46
  29. package/src/app/api/system/status/route.ts +11 -0
  30. package/src/app/api/upload/route.ts +4 -1
  31. package/src/cli/index.js +7 -0
  32. package/src/cli/spec.js +1 -0
  33. package/src/components/agents/agent-files-editor.tsx +44 -32
  34. package/src/components/agents/personality-builder.tsx +13 -7
  35. package/src/components/agents/trash-list.tsx +1 -1
  36. package/src/components/chat/chat-area.tsx +45 -23
  37. package/src/components/chat/message-bubble.test.ts +35 -0
  38. package/src/components/chat/message-bubble.tsx +20 -9
  39. package/src/components/chat/message-list.tsx +62 -42
  40. package/src/components/chat/swarm-status-card.tsx +10 -3
  41. package/src/components/input/chat-input.tsx +34 -14
  42. package/src/components/layout/daemon-indicator.tsx +7 -8
  43. package/src/components/layout/update-banner.tsx +8 -13
  44. package/src/components/logs/log-list.tsx +1 -1
  45. package/src/components/memory/memory-card.tsx +3 -1
  46. package/src/components/org-chart/org-chart-view.tsx +4 -0
  47. package/src/components/projects/project-list.tsx +4 -2
  48. package/src/components/projects/tabs/overview-tab.tsx +3 -2
  49. package/src/components/secrets/secret-sheet.tsx +1 -1
  50. package/src/components/secrets/secrets-list.tsx +1 -1
  51. package/src/components/shared/agent-switch-dialog.tsx +12 -6
  52. package/src/components/shared/dir-browser.tsx +22 -18
  53. package/src/components/skills/skill-sheet.tsx +2 -3
  54. package/src/components/tasks/task-list.tsx +1 -1
  55. package/src/components/tasks/task-sheet.tsx +1 -1
  56. package/src/hooks/use-openclaw-gateway.ts +46 -27
  57. package/src/instrumentation.ts +10 -7
  58. package/src/lib/chat/assistant-render-id.ts +3 -0
  59. package/src/lib/chat/chat-streaming-state.test.ts +42 -3
  60. package/src/lib/chat/chat-streaming-state.ts +20 -8
  61. package/src/lib/chat/chat.ts +18 -2
  62. package/src/lib/chat/queued-message-queue.test.ts +23 -1
  63. package/src/lib/chat/queued-message-queue.ts +11 -2
  64. package/src/lib/providers/anthropic.ts +6 -3
  65. package/src/lib/providers/claude-cli.ts +9 -3
  66. package/src/lib/providers/cli-utils.test.ts +124 -0
  67. package/src/lib/providers/cli-utils.ts +15 -0
  68. package/src/lib/providers/codex-cli.ts +9 -3
  69. package/src/lib/providers/gemini-cli.ts +6 -2
  70. package/src/lib/providers/index.ts +4 -1
  71. package/src/lib/providers/ollama.ts +5 -2
  72. package/src/lib/providers/openai.ts +8 -5
  73. package/src/lib/providers/opencode-cli.ts +6 -2
  74. package/src/lib/server/activity/activity-log.ts +21 -0
  75. package/src/lib/server/agents/agent-availability.test.ts +10 -5
  76. package/src/lib/server/agents/agent-cascade.ts +79 -59
  77. package/src/lib/server/agents/agent-registry.ts +23 -4
  78. package/src/lib/server/agents/agent-repository.ts +90 -0
  79. package/src/lib/server/agents/delegation-job-repository.ts +53 -0
  80. package/src/lib/server/agents/delegation-jobs.ts +11 -4
  81. package/src/lib/server/agents/guardian-checkpoint-repository.ts +35 -0
  82. package/src/lib/server/agents/guardian.ts +2 -2
  83. package/src/lib/server/agents/main-agent-loop.ts +14 -6
  84. package/src/lib/server/agents/main-loop-state-repository.ts +38 -0
  85. package/src/lib/server/agents/subagent-runtime.ts +9 -6
  86. package/src/lib/server/agents/subagent-swarm.ts +3 -2
  87. package/src/lib/server/agents/task-session.ts +3 -4
  88. package/src/lib/server/approvals/approval-repository.ts +30 -0
  89. package/src/lib/server/autonomy/supervisor-incident-repository.ts +42 -0
  90. package/src/lib/server/autonomy/supervisor-reflection.ts +14 -1
  91. package/src/lib/server/chat-execution/chat-execution-types.ts +38 -0
  92. package/src/lib/server/chat-execution/chat-execution-utils.ts +1 -1
  93. package/src/lib/server/chat-execution/chat-execution.ts +84 -1914
  94. package/src/lib/server/chat-execution/chat-turn-finalization.ts +620 -0
  95. package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +221 -0
  96. package/src/lib/server/chat-execution/chat-turn-preflight.ts +133 -0
  97. package/src/lib/server/chat-execution/chat-turn-preparation.ts +817 -0
  98. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +296 -0
  99. package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +5 -5
  100. package/src/lib/server/chat-execution/continuation-evaluator.ts +4 -3
  101. package/src/lib/server/chat-execution/continuation-limits.ts +6 -3
  102. package/src/lib/server/chat-execution/message-classifier.test.ts +329 -0
  103. package/src/lib/server/chat-execution/message-classifier.ts +5 -2
  104. package/src/lib/server/chat-execution/post-stream-finalization.ts +5 -2
  105. package/src/lib/server/chat-execution/prompt-builder.ts +22 -1
  106. package/src/lib/server/chat-execution/prompt-sections.ts +55 -13
  107. package/src/lib/server/chat-execution/response-completeness.ts +5 -2
  108. package/src/lib/server/chat-execution/situational-awareness.ts +12 -7
  109. package/src/lib/server/chat-execution/stream-agent-chat.ts +58 -25
  110. package/src/lib/server/chatrooms/chatroom-memory-bridge.ts +6 -3
  111. package/src/lib/server/chatrooms/chatroom-repository.ts +32 -0
  112. package/src/lib/server/connectors/bluebubbles.ts +7 -4
  113. package/src/lib/server/connectors/connector-inbound.ts +16 -13
  114. package/src/lib/server/connectors/connector-lifecycle.ts +11 -8
  115. package/src/lib/server/connectors/connector-outbound.ts +6 -3
  116. package/src/lib/server/connectors/connector-repository.ts +58 -0
  117. package/src/lib/server/connectors/discord.ts +10 -7
  118. package/src/lib/server/connectors/email.ts +17 -14
  119. package/src/lib/server/connectors/googlechat.ts +7 -4
  120. package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -2
  121. package/src/lib/server/connectors/matrix.ts +6 -3
  122. package/src/lib/server/connectors/openclaw.ts +20 -17
  123. package/src/lib/server/connectors/outbox.ts +4 -1
  124. package/src/lib/server/connectors/runtime-state.test.ts +117 -0
  125. package/src/lib/server/connectors/runtime-state.ts +19 -0
  126. package/src/lib/server/connectors/session-consolidation.ts +5 -2
  127. package/src/lib/server/connectors/signal.ts +9 -6
  128. package/src/lib/server/connectors/slack.ts +13 -10
  129. package/src/lib/server/connectors/teams.ts +8 -5
  130. package/src/lib/server/connectors/telegram.ts +15 -12
  131. package/src/lib/server/connectors/whatsapp.ts +32 -29
  132. package/src/lib/server/credentials/credential-repository.ts +7 -0
  133. package/src/lib/server/embeddings.ts +4 -1
  134. package/src/lib/server/gateways/gateway-profile-repository.ts +4 -0
  135. package/src/lib/server/link-understanding.ts +4 -1
  136. package/src/lib/server/memory/memory-abstract.test.ts +59 -0
  137. package/src/lib/server/memory/memory-abstract.ts +59 -0
  138. package/src/lib/server/memory/memory-db.ts +40 -14
  139. package/src/lib/server/missions/mission-repository.ts +74 -0
  140. package/src/lib/server/missions/mission-service/actions.ts +6 -0
  141. package/src/lib/server/missions/mission-service/bindings.ts +9 -0
  142. package/src/lib/server/missions/mission-service/context.ts +4 -0
  143. package/src/lib/server/missions/mission-service/core.ts +2269 -0
  144. package/src/lib/server/missions/mission-service/queries.ts +12 -0
  145. package/src/lib/server/missions/mission-service/recovery.ts +5 -0
  146. package/src/lib/server/missions/mission-service/ticks.ts +9 -0
  147. package/src/lib/server/missions/mission-service.test.ts +9 -2
  148. package/src/lib/server/missions/mission-service.ts +6 -2263
  149. package/src/lib/server/openclaw/gateway.ts +8 -5
  150. package/src/lib/server/persistence/repository-utils.ts +154 -0
  151. package/src/lib/server/persistence/storage-context.ts +51 -0
  152. package/src/lib/server/persistence/transaction.ts +1 -0
  153. package/src/lib/server/project-utils.ts +13 -0
  154. package/src/lib/server/projects/project-repository.ts +36 -0
  155. package/src/lib/server/projects/project-service.ts +79 -0
  156. package/src/lib/server/protocols/protocol-agent-turn.ts +5 -2
  157. package/src/lib/server/protocols/protocol-normalization.test.ts +6 -4
  158. package/src/lib/server/protocols/protocol-run-lifecycle.ts +5 -2
  159. package/src/lib/server/protocols/protocol-step-helpers.ts +4 -1
  160. package/src/lib/server/provider-health.ts +18 -0
  161. package/src/lib/server/query-expansion.ts +4 -1
  162. package/src/lib/server/runtime/alert-dispatch.ts +8 -7
  163. package/src/lib/server/runtime/daemon-policy.ts +1 -1
  164. package/src/lib/server/runtime/daemon-state/core.ts +1570 -0
  165. package/src/lib/server/runtime/daemon-state/health.ts +6 -0
  166. package/src/lib/server/runtime/daemon-state/policy.ts +7 -0
  167. package/src/lib/server/runtime/daemon-state/supervisor.ts +6 -0
  168. package/src/lib/server/runtime/daemon-state.test.ts +48 -0
  169. package/src/lib/server/runtime/daemon-state.ts +3 -1331
  170. package/src/lib/server/runtime/estop-repository.ts +4 -0
  171. package/src/lib/server/runtime/estop.ts +3 -1
  172. package/src/lib/server/runtime/heartbeat-service.test.ts +2 -2
  173. package/src/lib/server/runtime/heartbeat-service.ts +78 -34
  174. package/src/lib/server/runtime/heartbeat-wake.ts +6 -4
  175. package/src/lib/server/runtime/idle-window.ts +6 -3
  176. package/src/lib/server/runtime/network.ts +11 -0
  177. package/src/lib/server/runtime/orchestrator-events.ts +2 -2
  178. package/src/lib/server/runtime/perf.ts +4 -1
  179. package/src/lib/server/runtime/process-manager.ts +7 -4
  180. package/src/lib/server/runtime/queue/claims.ts +4 -0
  181. package/src/lib/server/runtime/queue/core.ts +2079 -0
  182. package/src/lib/server/runtime/queue/execution.ts +7 -0
  183. package/src/lib/server/runtime/queue/followups.ts +4 -0
  184. package/src/lib/server/runtime/queue/queries.ts +12 -0
  185. package/src/lib/server/runtime/queue/recovery.ts +7 -0
  186. package/src/lib/server/runtime/queue-recovery.test.ts +48 -13
  187. package/src/lib/server/runtime/queue-repository.ts +17 -0
  188. package/src/lib/server/runtime/queue.ts +5 -2058
  189. package/src/lib/server/runtime/run-ledger.ts +6 -5
  190. package/src/lib/server/runtime/run-repository.ts +73 -0
  191. package/src/lib/server/runtime/runtime-lock-repository.ts +8 -0
  192. package/src/lib/server/runtime/runtime-settings.ts +1 -1
  193. package/src/lib/server/runtime/runtime-state.ts +99 -0
  194. package/src/lib/server/runtime/scheduler.ts +13 -8
  195. package/src/lib/server/runtime/session-run-manager/cancellation.ts +157 -0
  196. package/src/lib/server/runtime/session-run-manager/drain.ts +246 -0
  197. package/src/lib/server/runtime/session-run-manager/enqueue.ts +287 -0
  198. package/src/lib/server/runtime/session-run-manager/queries.ts +117 -0
  199. package/src/lib/server/runtime/session-run-manager/recovery.ts +238 -0
  200. package/src/lib/server/runtime/session-run-manager/state.ts +441 -0
  201. package/src/lib/server/runtime/session-run-manager/types.ts +74 -0
  202. package/src/lib/server/runtime/session-run-manager.ts +72 -1374
  203. package/src/lib/server/runtime/watch-job-repository.ts +35 -0
  204. package/src/lib/server/runtime/watch-jobs.ts +3 -1
  205. package/src/lib/server/sandbox/bridge-auth-registry.ts +6 -0
  206. package/src/lib/server/sandbox/novnc-auth.ts +10 -0
  207. package/src/lib/server/schedules/schedule-repository.ts +42 -0
  208. package/src/lib/server/session-tools/context.ts +14 -0
  209. package/src/lib/server/session-tools/discovery.ts +9 -6
  210. package/src/lib/server/session-tools/index.ts +3 -1
  211. package/src/lib/server/session-tools/platform.ts +1 -1
  212. package/src/lib/server/session-tools/subagent.ts +23 -2
  213. package/src/lib/server/session-tools/wallet.ts +4 -1
  214. package/src/lib/server/sessions/session-repository.ts +85 -0
  215. package/src/lib/server/settings/settings-repository.ts +25 -0
  216. package/src/lib/server/skills/clawhub-client.ts +4 -1
  217. package/src/lib/server/skills/runtime-skill-resolver.ts +8 -2
  218. package/src/lib/server/skills/skill-discovery.test.ts +2 -2
  219. package/src/lib/server/skills/skill-discovery.ts +2 -2
  220. package/src/lib/server/skills/skill-eligibility.ts +6 -0
  221. package/src/lib/server/skills/skill-repository.ts +14 -0
  222. package/src/lib/server/solana.ts +6 -0
  223. package/src/lib/server/storage-auth.ts +5 -5
  224. package/src/lib/server/storage-normalization.ts +4 -0
  225. package/src/lib/server/storage.ts +32 -32
  226. package/src/lib/server/tasks/task-followups.ts +4 -1
  227. package/src/lib/server/tasks/task-repository.ts +54 -0
  228. package/src/lib/server/tool-loop-detection.ts +8 -3
  229. package/src/lib/server/tool-planning.ts +226 -0
  230. package/src/lib/server/tool-retry.ts +4 -3
  231. package/src/lib/server/usage/usage-repository.ts +30 -0
  232. package/src/lib/server/wallet/wallet-portfolio.ts +29 -0
  233. package/src/lib/server/webhooks/webhook-repository.ts +10 -0
  234. package/src/lib/server/ws-hub.ts +5 -2
  235. package/src/lib/strip-internal-metadata.test.ts +78 -37
  236. package/src/lib/strip-internal-metadata.ts +20 -6
  237. package/src/stores/use-approval-store.ts +7 -1
  238. package/src/stores/use-chat-store.test.ts +54 -0
  239. package/src/stores/use-chat-store.ts +26 -6
  240. package/src/types/index.ts +6 -0
  241. /package/{bundled-skills → skills}/google-workspace/SKILL.md +0 -0
@@ -2,6 +2,9 @@ import { WebSocketServer, WebSocket } from 'ws'
2
2
  import type { IncomingMessage } from 'http'
3
3
  import { validateAccessKey } from './storage'
4
4
  import { AUTH_COOKIE_NAME, getCookieValue } from '@/lib/auth'
5
+ import { log } from '@/lib/server/logger'
6
+
7
+ const TAG = 'ws-hub'
5
8
 
6
9
  interface WsClient {
7
10
  ws: WebSocket
@@ -67,10 +70,10 @@ export function initWsServer() {
67
70
  })
68
71
 
69
72
  wss.on('error', (err) => {
70
- console.error('[ws-hub] WebSocket server error:', err.message)
73
+ log.error(TAG, 'WebSocket server error:', err.message)
71
74
  })
72
75
 
73
- console.log(`[ws-hub] WebSocket server listening on port ${port}`)
76
+ log.info(TAG, `WebSocket server listening on port ${port}`)
74
77
  }
75
78
 
76
79
  export function closeWsServer(): Promise<void> {
@@ -1,4 +1,5 @@
1
- import { describe, it, expect } from 'vitest'
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it } from 'node:test'
2
3
  import { stripInternalJson, stripLoopDetectionMessages, stripAllInternalMetadata } from './strip-internal-metadata'
3
4
 
4
5
  // ---------------------------------------------------------------------------
@@ -8,37 +9,37 @@ import { stripInternalJson, stripLoopDetectionMessages, stripAllInternalMetadata
8
9
  describe('stripInternalJson', () => {
9
10
  it('removes single-line classification JSON', () => {
10
11
  const input = '{ "isDeliverableTask": true, "quality_score": 0.8, "isBroadGoal": false }'
11
- expect(stripInternalJson(input).trim()).toBe('')
12
+ assert.equal(stripInternalJson(input).trim(), '')
12
13
  })
13
14
 
14
15
  it('removes classification JSON embedded in surrounding text', () => {
15
16
  const input = 'Here is the answer.\n{ "isDeliverableTask": true, "confidence": 0.9 }\nMore text follows.'
16
17
  const result = stripInternalJson(input)
17
- expect(result).toContain('Here is the answer.')
18
- expect(result).toContain('More text follows.')
19
- expect(result).not.toContain('isDeliverableTask')
18
+ assert.match(result, /Here is the answer\./)
19
+ assert.match(result, /More text follows\./)
20
+ assert.doesNotMatch(result, /isDeliverableTask/)
20
21
  })
21
22
 
22
23
  it('preserves legitimate JSON that does not contain internal keys', () => {
23
24
  const input = 'The config is { "name": "test", "port": 3000 }'
24
- expect(stripInternalJson(input)).toBe(input)
25
+ assert.equal(stripInternalJson(input), input)
25
26
  })
26
27
 
27
28
  it('preserves JSON with nested objects if no internal keys', () => {
28
29
  const input = '{ "user": { "name": "alice" } }'
29
- expect(stripInternalJson(input)).toBe(input)
30
+ assert.equal(stripInternalJson(input), input)
30
31
  })
31
32
 
32
33
  it('removes JSON with nested objects when internal keys are present', () => {
33
34
  const input = '{ "walletIntent": "send", "details": { "amount": 100 } }'
34
- expect(stripInternalJson(input).trim()).toBe('')
35
+ assert.equal(stripInternalJson(input).trim(), '')
35
36
  })
36
37
 
37
38
  it('handles multiple JSON blocks, only removing internal ones', () => {
38
39
  const input = '{ "isDeliverableTask": true } some text { "foo": "bar" }'
39
40
  const result = stripInternalJson(input)
40
- expect(result).not.toContain('isDeliverableTask')
41
- expect(result).toContain('{ "foo": "bar" }')
41
+ assert.doesNotMatch(result, /isDeliverableTask/)
42
+ assert.match(result, /\{ "foo": "bar" \}/)
42
43
  })
43
44
  })
44
45
 
@@ -49,70 +50,110 @@ describe('stripInternalJson', () => {
49
50
  describe('stripLoopDetectionMessages', () => {
50
51
  it('strips tool frequency "called N times" messages', () => {
51
52
  const input = 'Tool "shell" called 30 times this turn. Excessive repetition — wrap up with available results.'
52
- expect(stripLoopDetectionMessages(input).trim()).toBe('')
53
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
53
54
  })
54
55
 
55
56
  it('strips tool frequency "would be called" messages', () => {
56
57
  const input = 'Tool "shell" would be called 31 times this turn. Excessive repetition — wrap up with available results.'
57
- expect(stripLoopDetectionMessages(input).trim()).toBe('')
58
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
58
59
  })
59
60
 
60
61
  it('strips "nearing overuse" messages', () => {
61
62
  const input = 'Tool "read" is nearing overuse (15 calls this turn). Consider whether another call is needed.'
62
- expect(stripLoopDetectionMessages(input).trim()).toBe('')
63
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
63
64
  })
64
65
 
65
- it('strips generic repeat "has been called" messages', () => {
66
- const input = 'Tool "search" has been called 12 times with the same input. This appears to be a stuck loop.'
67
- expect(stripLoopDetectionMessages(input).trim()).toBe('')
66
+ it('strips generic repeat "You called" messages', () => {
67
+ const input = 'You called "browser" 6 times with identical input. Input: "{"action":"screenshot"}" State your blocker or deliver what you have.'
68
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
69
+ })
70
+
71
+ it('strips generic repeat "You called" warning messages', () => {
72
+ const input = 'You called "search" 4 times with identical input. Input: "query" — Try a fundamentally different approach or deliver partial results.'
73
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
68
74
  })
69
75
 
70
76
  it('strips "would repeat the same input" messages', () => {
71
- const input = 'Tool "search" would repeat the same input 12 times. Blocking before it becomes a stuck loop.'
72
- expect(stripLoopDetectionMessages(input).trim()).toBe('')
77
+ const input = '"search" would repeat the same input 12 times. Input: "query" State your blocker or deliver what you have.'
78
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
73
79
  })
74
80
 
75
81
  it('strips "is about to repeat the same input" messages', () => {
76
- const input = 'Tool "search" is about to repeat the same input 6 times. Consider a different approach.'
77
- expect(stripLoopDetectionMessages(input).trim()).toBe('')
82
+ const input = '"search" is about to repeat the same input 6 times. Input: "query" — Try a different approach.'
83
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
78
84
  })
79
85
 
80
86
  it('strips circuit breaker messages', () => {
81
87
  const input = 'Circuit breaker: "shell" called 20 times with identical input. Halting to prevent runaway.'
82
- expect(stripLoopDetectionMessages(input).trim()).toBe('')
88
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
83
89
  })
84
90
 
85
91
  it('strips circuit breaker preview messages', () => {
86
92
  const input = 'Circuit breaker: "shell" would be called 20 times with identical input. Halting before another runaway call.'
87
- expect(stripLoopDetectionMessages(input).trim()).toBe('')
93
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
88
94
  })
89
95
 
90
96
  it('strips polling stall messages', () => {
91
97
  const input = 'Polling stall: "status_check" returned identical output 8 times consecutively. The polled resource is not changing.'
92
- expect(stripLoopDetectionMessages(input).trim()).toBe('')
98
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
93
99
  })
94
100
 
95
101
  it('strips ping-pong "are alternating" messages', () => {
96
102
  const input = 'Ping-pong: "read" and "write" are alternating with identical results (5 cycles). Breaking the loop.'
97
- expect(stripLoopDetectionMessages(input).trim()).toBe('')
103
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
98
104
  })
99
105
 
100
106
  it('strips ping-pong "may be stuck" messages', () => {
101
107
  const input = 'Ping-pong: "read" and "write" may be stuck in an alternating loop (3 cycles).'
102
- expect(stripLoopDetectionMessages(input).trim()).toBe('')
108
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
109
+ })
110
+
111
+ it('strips output stagnation messages', () => {
112
+ const input = 'Output stagnation: last 8 tool calls all produced identical output. The approach is not working — try something fundamentally different or report the blocker.'
113
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
114
+ })
115
+
116
+ it('strips output stagnation warning messages', () => {
117
+ const input = 'Output stagnation: 6 of the last 8 tool calls produced identical output. Your tools may not be making progress.'
118
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
119
+ })
120
+
121
+ it('strips error convergence messages', () => {
122
+ const input = 'Error convergence: 5 of the last 6 tool calls returned errors. Stop retrying and report the underlying issue (likely an infrastructure or configuration problem).'
123
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
124
+ })
125
+
126
+ it('strips error convergence warning messages', () => {
127
+ const input = 'Error convergence: 4 of the last 6 tool calls returned errors. You may be hitting a systemic issue — consider a different approach or report the blocker.'
128
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
129
+ })
130
+
131
+ it('strips messages wrapped in [Error: ...] brackets', () => {
132
+ const input = '[Error: You called "browser" 6 times with identical input. Input: "{"action":"screenshot"}" — State your blocker or deliver what you have.]'
133
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
134
+ })
135
+
136
+ it('strips [Error: ...] wrapped tool frequency messages', () => {
137
+ const input = '[Error: Tool "shell" called 30 times this turn. Excessive repetition — wrap up with available results.]'
138
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
139
+ })
140
+
141
+ it('strips [Error: ...] wrapped output stagnation messages', () => {
142
+ const input = '[Error: Output stagnation: last 8 tool calls all produced identical output.]'
143
+ assert.equal(stripLoopDetectionMessages(input).trim(), '')
103
144
  })
104
145
 
105
146
  it('preserves normal text that mentions tools', () => {
106
147
  const input = 'I used the shell tool to run the command.'
107
- expect(stripLoopDetectionMessages(input)).toBe(input)
148
+ assert.equal(stripLoopDetectionMessages(input), input)
108
149
  })
109
150
 
110
151
  it('strips loop message embedded in surrounding text', () => {
111
152
  const input = 'Working on it.\nTool "shell" called 30 times this turn. Excessive repetition — wrap up with available results.\nHere are the results.'
112
153
  const result = stripLoopDetectionMessages(input)
113
- expect(result).toContain('Working on it.')
114
- expect(result).toContain('Here are the results.')
115
- expect(result).not.toContain('called 30 times')
154
+ assert.match(result, /Working on it\./)
155
+ assert.match(result, /Here are the results\./)
156
+ assert.doesNotMatch(result, /called 30 times/)
116
157
  })
117
158
  })
118
159
 
@@ -129,32 +170,32 @@ describe('stripAllInternalMetadata', () => {
129
170
  'The answer is 42.',
130
171
  ].join('\n')
131
172
  const result = stripAllInternalMetadata(input)
132
- expect(result).not.toContain('isDeliverableTask')
133
- expect(result).not.toContain('called 30 times')
134
- expect(result).toContain('Here is my analysis.')
135
- expect(result).toContain('The answer is 42.')
173
+ assert.doesNotMatch(result, /isDeliverableTask/)
174
+ assert.doesNotMatch(result, /called 30 times/)
175
+ assert.match(result, /Here is my analysis\./)
176
+ assert.match(result, /The answer is 42\./)
136
177
  })
137
178
 
138
179
  it('collapses excessive newlines after stripping', () => {
139
180
  const input = 'Hello.\n\n\n\n{ "isDeliverableTask": true }\n\n\n\nWorld.'
140
181
  const result = stripAllInternalMetadata(input)
141
- expect(result).not.toMatch(/\n{3,}/)
182
+ assert.doesNotMatch(result, /\n{3,}/)
142
183
  })
143
184
 
144
185
  it('returns empty string for purely internal content', () => {
145
186
  const input = '{ "isDeliverableTask": true, "quality_score": 0.9 }'
146
- expect(stripAllInternalMetadata(input)).toBe('')
187
+ assert.equal(stripAllInternalMetadata(input), '')
147
188
  })
148
189
 
149
190
  it('leaves normal messages untouched', () => {
150
191
  const input = 'Here is a normal response with no internal metadata.'
151
- expect(stripAllInternalMetadata(input)).toBe(input)
192
+ assert.equal(stripAllInternalMetadata(input), input)
152
193
  })
153
194
 
154
195
  it('preserves code blocks with JSON that happen to have similar-looking keys', () => {
155
196
  // JSON in code blocks uses real braces, so the regex will match the block.
156
197
  // But since 'name' and 'age' are not internal keys, it should be preserved.
157
198
  const input = 'Result: { "name": "Alice", "age": 30 }'
158
- expect(stripAllInternalMetadata(input)).toBe(input)
199
+ assert.equal(stripAllInternalMetadata(input), input)
159
200
  })
160
201
  })
@@ -43,35 +43,49 @@ export function stripInternalJson(text: string): string {
43
43
  * - Tool "X" called N times ...
44
44
  * - Tool "X" would be called N times ...
45
45
  * - Tool "X" is nearing overuse ...
46
- * - Tool "X" has been called N times with the same input ...
47
- * - Tool "X" would repeat the same input N times ...
48
- * - Tool "X" is about to repeat the same input N times ...
46
+ * - You called "X" N times with identical input ...
47
+ * - "X" would repeat the same input N times ...
48
+ * - "X" is about to repeat the same input N times ...
49
49
  * - Circuit breaker: "X" called N times ...
50
50
  * - Circuit breaker: "X" would be called N times ...
51
51
  * - Polling stall: "X" returned identical output N times ...
52
52
  * - Ping-pong: "X" and "Y" are alternating ...
53
53
  * - Ping-pong: "X" and "Y" may be stuck ...
54
+ * - Output stagnation: last N / N of the last N ...
55
+ * - Error convergence: N of the last N ...
54
56
  */
55
57
  const LOOP_DETECTION_RE = new RegExp(
56
58
  [
57
59
  // Tool frequency: called / would be called / nearing overuse
58
60
  String.raw`Tool "[^"]*" (?:called|would be called) \d+ times[^\n]*`,
59
61
  String.raw`Tool "[^"]*" is nearing overuse[^\n]*`,
60
- // Generic repeat: has been called / would repeat / is about to repeat
61
- String.raw`Tool "[^"]*" (?:has been called|would repeat the same input|is about to repeat the same input) \d+ times[^\n]*`,
62
+ // Generic repeat: "You called" (post-call) / "X" would repeat / is about to repeat (preview)
63
+ String.raw`You called "[^"]*" \d+ times[^\n]*`,
64
+ String.raw`"[^"]*" (?:would repeat the same input|is about to repeat the same input) \d+ times[^\n]*`,
62
65
  // Circuit breaker
63
66
  String.raw`Circuit breaker: "[^"]*" (?:called|would be called) \d+ times[^\n]*`,
64
67
  // Polling stall
65
68
  String.raw`Polling stall: "[^"]*" returned identical output \d+ times[^\n]*`,
66
69
  // Ping-pong
67
70
  String.raw`Ping-pong: "[^"]*" and "[^"]*" (?:are alternating|may be stuck)[^\n]*`,
71
+ // Output stagnation
72
+ String.raw`Output stagnation:[^\n]*`,
73
+ // Error convergence
74
+ String.raw`Error convergence:[^\n]*`,
68
75
  ].join('|'),
69
76
  'g',
70
77
  )
71
78
 
79
+ /**
80
+ * Matches loop detection messages wrapped in `[Error: ...]` brackets
81
+ * (from the err SSE event handler in use-chat-store.ts).
82
+ */
83
+ const LOOP_DETECTION_WRAPPED_RE = /\[Error: (?:Tool "[^"]*" (?:called|would be called) \d+ times|Tool "[^"]*" is nearing overuse|You called "[^"]*" \d+ times|"[^"]*" (?:would repeat the same input|is about to repeat the same input) \d+ times|Circuit breaker: "[^"]*" (?:called|would be called) \d+ times|Polling stall: "[^"]*" returned identical output \d+ times|Ping-pong: "[^"]*" and "[^"]*" (?:are alternating|may be stuck)|Output stagnation:|Error convergence:)[^\]]*\]/g
84
+
72
85
  /** Remove loop detection messages that the LLM echoed from tool error results. */
73
86
  export function stripLoopDetectionMessages(text: string): string {
74
- return text.replace(LOOP_DETECTION_RE, '')
87
+ // Strip [Error: ...] wrapped versions first, before the inner regex eats the content
88
+ return text.replace(LOOP_DETECTION_WRAPPED_RE, '').replace(LOOP_DETECTION_RE, '')
75
89
  }
76
90
 
77
91
  // ---------------------------------------------------------------------------
@@ -76,7 +76,13 @@ export const useApprovalStore = create<ApprovalState>((set) => ({
76
76
  for (const [id, a] of Object.entries(s.approvals)) {
77
77
  if (a.expiresAtMs > now && !s.resolvedIds.has(id)) next[id] = a
78
78
  }
79
- return { approvals: next }
79
+ // Prune resolvedIds for IDs no longer in the approval set
80
+ const liveIds = new Set(Object.keys(next))
81
+ const nextResolved = new Set<string>()
82
+ for (const id of s.resolvedIds) {
83
+ if (liveIds.has(id)) nextResolved.add(id)
84
+ }
85
+ return { approvals: next, resolvedIds: nextResolved }
80
86
  })
81
87
  },
82
88
 
@@ -196,6 +196,60 @@ describe('useChatStore control-token hygiene', () => {
196
196
  assert.equal(assistantMessage?.clientRenderId, state.assistantRenderId)
197
197
  })
198
198
 
199
+ it('marks the session idle locally as soon as a direct stream finishes', async () => {
200
+ const session = makeSession({ active: true, currentRunId: 'run-1' } as Partial<Session>)
201
+ useAppStore.setState({
202
+ agents: { 'agent-1': makeAgent() },
203
+ sessions: { [session.id]: session },
204
+ currentAgentId: 'agent-1',
205
+ })
206
+ useChatStore.setState({
207
+ messages: [],
208
+ pendingFiles: [],
209
+ replyingTo: null,
210
+ toolEvents: [],
211
+ streamText: '',
212
+ displayText: '',
213
+ streaming: false,
214
+ streamingSessionId: null,
215
+ streamSource: null,
216
+ assistantRenderId: null,
217
+ streamPhase: 'thinking',
218
+ streamToolName: '',
219
+ thinkingText: '',
220
+ thinkingStartTime: 0,
221
+ queuedMessages: [],
222
+ agentStatus: null,
223
+ lastUsage: null,
224
+ hasMoreMessages: false,
225
+ loadingMore: false,
226
+ totalMessages: 0,
227
+ })
228
+
229
+ global.fetch = (async (input: RequestInfo | URL) => {
230
+ const url = String(input)
231
+ if (url === '/api/chats/session-1/chat') {
232
+ return sseResponse([
233
+ { t: 'r', text: 'Done' },
234
+ { t: 'done' },
235
+ ])
236
+ }
237
+ if (url === '/api/chats/session-1') {
238
+ return new Response(JSON.stringify({ ...session, active: false, currentRunId: null }), {
239
+ status: 200,
240
+ headers: { 'content-type': 'application/json' },
241
+ })
242
+ }
243
+ throw new Error(`Unexpected fetch: ${url}`)
244
+ }) as unknown as typeof fetch
245
+
246
+ await useChatStore.getState().sendMessage('Hello', { sessionId: 'session-1' })
247
+
248
+ const refreshedSession = useAppStore.getState().sessions['session-1']
249
+ assert.equal(refreshedSession?.active, false)
250
+ assert.equal(refreshedSession?.currentRunId, null)
251
+ })
252
+
199
253
  it('replaces optimistic queued items with the backend queue snapshot', async () => {
200
254
  const session = makeSession()
201
255
  useAppStore.setState({
@@ -10,6 +10,7 @@ import {
10
10
  removeQueuedSessionMessage,
11
11
  } from '@/lib/chat/chats'
12
12
  import { mergeCompletedAssistantMessage, reconcileClientMessageMetadata } from '@/lib/chat/chat-streaming-state'
13
+ import { createAssistantRenderId } from '@/lib/chat/assistant-render-id'
13
14
  import { stripAllInternalMetadata } from '@/lib/strip-internal-metadata'
14
15
  import {
15
16
  clearQueuedMessagesForSession,
@@ -152,10 +153,6 @@ function stripHiddenControlTokens(text: string): string {
152
153
  return cleaned.replace(/\n{3,}/g, '\n\n').trim()
153
154
  }
154
155
 
155
- function nextAssistantRenderId(): string {
156
- return `assistant-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`
157
- }
158
-
159
156
  function reconcileMessagesForState(
160
157
  nextMessages: Message[],
161
158
  currentMessages: Message[],
@@ -184,6 +181,17 @@ function syncSessionQueueState(sessionId: string, params: {
184
181
  })
185
182
  }
186
183
 
184
+ function markSessionRunIdle(sessionId: string): void {
185
+ const appState = useAppStore.getState()
186
+ const session = appState.sessions[sessionId]
187
+ if (!session) return
188
+ appState.updateSessionInStore({
189
+ ...session,
190
+ active: false,
191
+ currentRunId: null,
192
+ })
193
+ }
194
+
187
195
  export const useChatStore = create<ChatState>((set, get) => ({
188
196
  streaming: false,
189
197
  streamingSessionId: null,
@@ -239,6 +247,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
239
247
  s.queuedMessages,
240
248
  sessionId,
241
249
  snapshotToQueuedMessages(snapshot),
250
+ { activeRunId: snapshot.activeRunId },
242
251
  )
243
252
  // Clear "sending" items whose text has already appeared in chat messages
244
253
  const messages = s.messages
@@ -285,6 +294,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
285
294
  removeQueuedMessageById(s.queuedMessages, optimistic.runId),
286
295
  sessionId,
287
296
  snapshotToQueuedMessages(response.snapshot),
297
+ { activeRunId: response.snapshot.activeRunId },
288
298
  ),
289
299
  }))
290
300
  syncSessionQueueState(sessionId, {
@@ -314,6 +324,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
314
324
  s.queuedMessages,
315
325
  sessionId,
316
326
  snapshotToQueuedMessages(response.snapshot),
327
+ { activeRunId: response.snapshot.activeRunId },
317
328
  ),
318
329
  }))
319
330
  syncSessionQueueState(sessionId, {
@@ -331,6 +342,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
331
342
  s.queuedMessages,
332
343
  sessionId,
333
344
  snapshotToQueuedMessages(response.snapshot),
345
+ { activeRunId: response.snapshot.activeRunId },
334
346
  ),
335
347
  }))
336
348
  syncSessionQueueState(sessionId, {
@@ -388,7 +400,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
388
400
  attachedFiles,
389
401
  ...(replyToId ? { replyToId } : {}),
390
402
  }
391
- const assistantRenderId = nextAssistantRenderId()
403
+ const assistantRenderId = createAssistantRenderId()
392
404
  set((s) => ({
393
405
  streaming: true,
394
406
  streamingSessionId: sessionId,
@@ -418,7 +430,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
418
430
  let toolCallCounter = 0
419
431
  let soundFiredStart = false
420
432
  const shouldIgnoreTransientError = (msg: string) =>
421
- /cancelled by steer mode|stopped by user/i.test(msg || '')
433
+ /cancelled by steer mode|stopped by user|stream timed out/i.test(msg || '')
422
434
 
423
435
  try { await streamChat(sessionId, text, imagePath, imageUrl, (event: SSEEvent) => {
424
436
  // Forward events to voice conversation handler if active
@@ -603,6 +615,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
603
615
  thinkingText: '',
604
616
  thinkingStartTime: 0,
605
617
  }))
618
+ markSessionRunIdle(sessionId)
606
619
  if (get().ttsEnabled && !get().voiceConversationActive) speak(visibleFinalText)
607
620
  } else {
608
621
  set({
@@ -617,6 +630,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
617
630
  thinkingText: '',
618
631
  thinkingStartTime: 0,
619
632
  })
633
+ markSessionRunIdle(sessionId)
620
634
  }
621
635
 
622
636
  void useAppStore.getState().refreshSession(sessionId)
@@ -635,6 +649,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
635
649
  thinkingText: '',
636
650
  thinkingStartTime: 0,
637
651
  })
652
+ markSessionRunIdle(sessionId)
638
653
  }
639
654
  }
640
655
  },
@@ -860,6 +875,11 @@ export const useChatStore = create<ChatState>((set, get) => ({
860
875
  displayText: '',
861
876
  streamPhase: 'thinking' as const,
862
877
  streamToolName: '',
878
+ thinkingText: '',
879
+ thinkingStartTime: 0,
880
+ toolEvents: [],
881
+ agentStatus: null,
863
882
  })
883
+ if (sessionId) markSessionRunIdle(sessionId)
864
884
  },
865
885
  }))
@@ -452,6 +452,8 @@ export interface Session {
452
452
  theme?: string
453
453
  avatar?: string
454
454
  canvasContent?: CanvasContent
455
+ /** Tracks how many times each memory ID has been injected via proactive recall in this session. */
456
+ injectedMemoryIds?: Record<string, number>
455
457
  }
456
458
 
457
459
  export type Sessions = Record<string, Session>
@@ -1545,6 +1547,7 @@ export interface MemoryEntry {
1545
1547
  lastAccessedAt?: number
1546
1548
  contentHash?: string
1547
1549
  reinforcementCount?: number
1550
+ abstract?: string | null
1548
1551
  createdAt: number
1549
1552
  updatedAt: number
1550
1553
  }
@@ -2147,6 +2150,9 @@ export interface SessionRunRecord {
2147
2150
  recoveredFromRestart?: boolean
2148
2151
  recoveredFromRunId?: string
2149
2152
  recoveryPayload?: SessionRunRecoveryPayload
2153
+ totalInputTokens?: number
2154
+ totalOutputTokens?: number
2155
+ estimatedCost?: number
2150
2156
  }
2151
2157
 
2152
2158
  export interface SessionQueuedTurn {