@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
@@ -43,16 +43,17 @@ describe('chat-streaming-state', () => {
43
43
  localStreaming: true,
44
44
  hasLiveArtifacts: true,
45
45
  assistantRenderId: 'render-1',
46
+ showLiveRow: true,
47
+ syntheticAssistant: messages[1],
46
48
  } as StreamingAwareMessageListOptions),
47
49
  [
48
50
  { role: 'user', text: 'hello', time: 1 },
49
51
  {
50
52
  role: 'assistant',
51
- text: 'final answer',
52
- time: 0,
53
+ text: 'partial',
54
+ time: 2,
53
55
  kind: 'chat',
54
56
  streaming: true,
55
- thinking: 'working...',
56
57
  clientRenderId: 'render-1',
57
58
  },
58
59
  ],
@@ -69,6 +70,7 @@ describe('chat-streaming-state', () => {
69
70
  localStreaming: true,
70
71
  hasLiveArtifacts: true,
71
72
  assistantRenderId: 'render-2',
73
+ showLiveRow: true,
72
74
  } as StreamingAwareMessageListOptions),
73
75
  [
74
76
  { role: 'user', text: 'hello', time: 1 },
@@ -84,6 +86,43 @@ describe('chat-streaming-state', () => {
84
86
  )
85
87
  })
86
88
 
89
+ it('can show a synthetic live row for server-driven runs using the latest persisted streaming artifact', () => {
90
+ const messages: Message[] = [
91
+ { role: 'user', text: 'queued follow-up', time: 1 },
92
+ {
93
+ role: 'assistant',
94
+ text: 'Drafting the handoff now.',
95
+ time: 2,
96
+ streaming: true,
97
+ thinking: 'Collecting the completion details',
98
+ toolEvents: [{ name: 'send_file', input: '{}', output: '/api/uploads/deck.pdf' }],
99
+ },
100
+ ]
101
+
102
+ assert.deepEqual(
103
+ buildStreamingAwareMessageList(messages, {
104
+ localStreaming: true,
105
+ hasLiveArtifacts: false,
106
+ assistantRenderId: 'render-server',
107
+ showLiveRow: true,
108
+ syntheticAssistant: messages[1],
109
+ }),
110
+ [
111
+ { role: 'user', text: 'queued follow-up', time: 1 },
112
+ {
113
+ role: 'assistant',
114
+ text: 'Drafting the handoff now.',
115
+ time: 2,
116
+ kind: 'chat',
117
+ streaming: true,
118
+ thinking: 'Collecting the completion details',
119
+ toolEvents: [{ name: 'send_file', input: '{}', output: '/api/uploads/deck.pdf' }],
120
+ clientRenderId: 'render-server',
121
+ },
122
+ ],
123
+ )
124
+ })
125
+
87
126
  it('replaces trailing streaming assistant messages with the completed assistant message', () => {
88
127
  const messages: Message[] = [
89
128
  { role: 'user', text: 'hello', time: 1 },
@@ -10,6 +10,8 @@ export interface StreamingAwareMessageListOptions {
10
10
  localStreaming: boolean
11
11
  hasLiveArtifacts: boolean
12
12
  assistantRenderId?: string | null
13
+ showLiveRow?: boolean
14
+ syntheticAssistant?: Partial<Message> | null
13
15
  }
14
16
 
15
17
  function isStreamingAssistantMessage(
@@ -27,13 +29,13 @@ function isStreamingAssistantMessage(
27
29
 
28
30
  export function shouldHidePersistedStreamingAssistantMessage(
29
31
  message: Message,
30
- opts: { localStreaming: boolean; hasLiveArtifacts: boolean },
32
+ opts: { localStreaming: boolean; hasLiveArtifacts: boolean; showLiveRow?: boolean },
31
33
  ): boolean {
34
+ const showLiveRow = opts.showLiveRow ?? (opts.localStreaming && opts.hasLiveArtifacts)
32
35
  return (
33
- opts.localStreaming
36
+ showLiveRow
34
37
  && message.role === 'assistant'
35
38
  && message.streaming === true
36
- && opts.hasLiveArtifacts
37
39
  )
38
40
  }
39
41
 
@@ -41,22 +43,32 @@ export function buildStreamingAwareMessageList(
41
43
  messages: Message[],
42
44
  opts: StreamingAwareMessageListOptions,
43
45
  ): Message[] {
44
- const nextMessages = opts.localStreaming && opts.hasLiveArtifacts
46
+ const showLiveRow = opts.showLiveRow ?? (opts.localStreaming && opts.hasLiveArtifacts)
47
+ const nextMessages = showLiveRow
45
48
  ? messages.filter((message) => !shouldHidePersistedStreamingAssistantMessage(message, opts))
46
49
  : messages
47
50
 
48
- if (!opts.localStreaming || !opts.hasLiveArtifacts || !opts.assistantRenderId) {
51
+ if (!showLiveRow || !opts.assistantRenderId) {
49
52
  return nextMessages
50
53
  }
51
54
 
52
55
  const syntheticAssistantMessage: Message = {
53
56
  role: 'assistant',
54
- text: '',
55
- time: 0,
56
- kind: 'chat',
57
+ text: typeof opts.syntheticAssistant?.text === 'string' ? opts.syntheticAssistant.text : '',
58
+ time: typeof opts.syntheticAssistant?.time === 'number' ? opts.syntheticAssistant.time : 0,
59
+ kind: opts.syntheticAssistant?.kind || 'chat',
57
60
  streaming: true,
58
61
  clientRenderId: opts.assistantRenderId,
59
62
  }
63
+ if (typeof opts.syntheticAssistant?.thinking === 'string') {
64
+ syntheticAssistantMessage.thinking = opts.syntheticAssistant.thinking
65
+ }
66
+ if (Array.isArray(opts.syntheticAssistant?.toolEvents)) {
67
+ syntheticAssistantMessage.toolEvents = opts.syntheticAssistant.toolEvents
68
+ }
69
+ if (opts.syntheticAssistant?.source) {
70
+ syntheticAssistantMessage.source = opts.syntheticAssistant.source
71
+ }
60
72
 
61
73
  return [
62
74
  ...nextMessages,
@@ -53,10 +53,26 @@ export async function streamChat(
53
53
  const reader = res.body.getReader()
54
54
  const decoder = new TextDecoder()
55
55
  let buf = ''
56
+ const STREAM_IDLE_TIMEOUT_MS = 300_000
56
57
 
57
58
  while (true) {
58
- const { done, value } = await reader.read()
59
- if (done) break
59
+ let timeoutId: ReturnType<typeof setTimeout> | undefined
60
+ let timedOut = false
61
+ const idleAbort = new Promise<{ done: true; value: undefined }>((resolve) => {
62
+ timeoutId = setTimeout(() => {
63
+ timedOut = true
64
+ resolve({ done: true, value: undefined })
65
+ }, STREAM_IDLE_TIMEOUT_MS)
66
+ })
67
+ const { done, value } = await Promise.race([reader.read(), idleAbort])
68
+ clearTimeout(timeoutId)
69
+ if (done) {
70
+ if (timedOut) {
71
+ onEvent?.({ t: 'err', text: 'Stream timed out (no data for 5 minutes)' })
72
+ reader.cancel().catch(() => {})
73
+ }
74
+ break
75
+ }
60
76
  buf += decoder.decode(value, { stream: true })
61
77
  const lines = buf.split('\n')
62
78
  buf = lines.pop() || ''
@@ -27,7 +27,7 @@ describe('queued-message-queue', () => {
27
27
  it('replaces queued items only for the requested session', () => {
28
28
  const replaced = replaceQueuedMessagesForSession(queue, 'session-a', [
29
29
  { runId: 'q4', sessionId: 'session-a', text: 'replacement', queuedAt: 4, position: 1 },
30
- ])
30
+ ], { activeRunId: null })
31
31
  assert.deepEqual(
32
32
  listQueuedMessagesForSession(replaced, 'session-a').map((item) => item.runId),
33
33
  ['q4'],
@@ -38,6 +38,28 @@ describe('queued-message-queue', () => {
38
38
  )
39
39
  })
40
40
 
41
+ it('keeps only the newly active run as a sending placeholder when it disappears from the queue snapshot', () => {
42
+ const replaced = replaceQueuedMessagesForSession(queue, 'session-a', [
43
+ { runId: 'q3', sessionId: 'session-a', text: 'second a', queuedAt: 3, position: 1 },
44
+ ], { activeRunId: 'q1' })
45
+
46
+ assert.deepEqual(
47
+ listQueuedMessagesForSession(replaced, 'session-a').map((item) => [item.runId, item.sending === true]),
48
+ [['q1', true], ['q3', false]],
49
+ )
50
+ })
51
+
52
+ it('drops missing stale queue rows that are not the active run', () => {
53
+ const replaced = replaceQueuedMessagesForSession(queue, 'session-a', [
54
+ { runId: 'q3', sessionId: 'session-a', text: 'second a', queuedAt: 3, position: 1 },
55
+ ], { activeRunId: 'run-other' })
56
+
57
+ assert.deepEqual(
58
+ listQueuedMessagesForSession(replaced, 'session-a').map((item) => item.runId),
59
+ ['q3'],
60
+ )
61
+ })
62
+
41
63
  it('removes queued items by stable id', () => {
42
64
  assert.deepEqual(removeQueuedMessageById(queue, 'q2').map((item) => item.runId), ['q1', 'q3'])
43
65
  })
@@ -41,18 +41,27 @@ export function snapshotToQueuedMessages(snapshot: SessionQueueSnapshot): Queued
41
41
  return snapshot.items.map((item) => ({ ...item }))
42
42
  }
43
43
 
44
+ interface ReplaceQueuedMessagesOptions {
45
+ activeRunId?: string | null
46
+ }
47
+
44
48
  export function replaceQueuedMessagesForSession(
45
49
  queue: QueuedSessionMessage[],
46
50
  sessionId: string,
47
51
  nextItems: QueuedSessionMessage[],
52
+ options: ReplaceQueuedMessagesOptions = {},
48
53
  ): QueuedSessionMessage[] {
49
54
  const otherSessions = queue.filter((item) => item.sessionId !== sessionId)
50
55
  const previousForSession = queue.filter((item) => item.sessionId === sessionId && !item.sending)
51
56
  // Detect consumed messages: items in local state but not in server snapshot.
52
- // Keep them visible as "sending" so they don't vanish from the UI.
57
+ // Keep only the run that actually became active visible as "sending" so it
58
+ // doesn't vanish from the UI before the transcript refresh catches up.
53
59
  const nextRunIds = new Set(nextItems.map((item) => item.runId))
60
+ const activeRunId = typeof options.activeRunId === 'string' && options.activeRunId.trim()
61
+ ? options.activeRunId
62
+ : null
54
63
  const consumed = previousForSession
55
- .filter((item) => !item.optimistic && !nextRunIds.has(item.runId))
64
+ .filter((item) => !item.optimistic && !nextRunIds.has(item.runId) && activeRunId === item.runId)
56
65
  .map((item) => ({ ...item, sending: true }))
57
66
  return [
58
67
  ...otherSessions,
@@ -2,8 +2,11 @@ import fs from 'fs'
2
2
  import https from 'https'
3
3
  import type { StreamChatOptions } from './index'
4
4
  import { PROVIDER_DEFAULTS, IMAGE_EXTS, TEXT_EXTS, ANTHROPIC_MAX_TOKENS, MAX_HISTORY_MESSAGES, writeSSE } from './provider-defaults'
5
+ import { log } from '@/lib/server/logger'
5
6
  import { resolveImagePath } from '@/lib/server/resolve-image'
6
7
 
8
+ const TAG = 'provider-anthropic'
9
+
7
10
  async function fileToContentBlocks(filePath: string): Promise<Array<Record<string, unknown>>> {
8
11
  if (!filePath || !fs.existsSync(filePath)) return []
9
12
  if (IMAGE_EXTS.test(filePath)) {
@@ -71,7 +74,7 @@ export function streamAnthropicChat({ session, message, imagePath, apiKey, syste
71
74
  apiRes.on('data', (c: Buffer) => errBody += c)
72
75
  apiRes.on('end', () => {
73
76
  const msg = `Anthropic error ${apiRes.statusCode}: ${errBody.slice(0, 200)}`
74
- console.error(`[${session.id}] ${msg}`)
77
+ log.error(TAG, `[${session.id}] ${msg}`)
75
78
  let errMsg = `Anthropic API error (${apiRes.statusCode})`
76
79
  try {
77
80
  const parsed = JSON.parse(errBody)
@@ -124,12 +127,12 @@ export function streamAnthropicChat({ session, message, imagePath, apiKey, syste
124
127
  active.set(session.id, { kill: () => { abortController.aborted = true; apiReq.destroy() } })
125
128
 
126
129
  apiReq.on('timeout', () => {
127
- console.error(`[${session.id}] anthropic request timed out after 60s`)
130
+ log.error(TAG, `[${session.id}] anthropic request timed out after 60s`)
128
131
  apiReq.destroy(new Error('Request timed out after 60s'))
129
132
  })
130
133
 
131
134
  apiReq.on('error', (e) => {
132
- console.error(`[${session.id}] anthropic request error:`, e.message)
135
+ log.error(TAG, `[${session.id}] anthropic request error:`, e.message)
133
136
  writeSSE(write, 'err', e.message)
134
137
  active.delete(session.id)
135
138
  reject(e)
@@ -6,7 +6,9 @@ import type { StreamChatOptions } from './index'
6
6
  import { log } from '../server/logger'
7
7
  import { loadRuntimeSettings } from '@/lib/server/runtime/runtime-settings'
8
8
  import { getEnabledToolIds } from '@/lib/capability-selection'
9
- import { resolveCliBinary, buildCliEnv, probeCliAuth, attachAbortHandler } from './cli-utils'
9
+ import { resolveCliBinary, buildCliEnv, probeCliAuth, attachAbortHandler, isStderrNoise } from './cli-utils'
10
+
11
+ const TAG = 'provider-claude-cli'
10
12
 
11
13
  export function streamClaudeCliChat({ session, message, imagePath, systemPrompt, write, active, signal }: StreamChatOptions): Promise<string> {
12
14
  const processTimeoutMs = loadRuntimeSettings().cliProcessTimeoutMs
@@ -152,8 +154,12 @@ export function streamClaudeCliChat({ session, message, imagePath, systemPrompt,
152
154
  const text = chunk.toString()
153
155
  stderrText += text
154
156
  if (stderrText.length > 16_000) stderrText = stderrText.slice(-16_000)
155
- log.warn('claude-cli', `stderr [${session.id}]`, text.slice(0, 500))
156
- console.error(`[${session.id}] stderr:`, text.slice(0, 200))
157
+ if (isStderrNoise(text)) {
158
+ log.debug('claude-cli', `stderr noise [${session.id}]`, text.slice(0, 500))
159
+ } else {
160
+ log.warn('claude-cli', `stderr [${session.id}]`, text.slice(0, 500))
161
+ log.error(TAG, `[${session.id}] stderr:`, text.slice(0, 200))
162
+ }
157
163
  })
158
164
 
159
165
  return new Promise((resolve) => {
@@ -0,0 +1,124 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it } from 'node:test'
3
+
4
+ import { isStderrNoise, buildCliEnv, isCliProvider, CLI_PROVIDER_CAPABILITIES } from './cli-utils'
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // isStderrNoise
8
+ // ---------------------------------------------------------------------------
9
+
10
+ describe('isStderrNoise', () => {
11
+ it('returns true for MallocStackLogging lines', () => {
12
+ assert.equal(isStderrNoise('MallocStackLogging: could not tag MSL'), true)
13
+ })
14
+
15
+ it('returns true for blank/whitespace lines', () => {
16
+ assert.equal(isStderrNoise(''), true)
17
+ assert.equal(isStderrNoise(' '), true)
18
+ assert.equal(isStderrNoise('\t'), true)
19
+ })
20
+
21
+ it('returns false for real error text', () => {
22
+ assert.equal(isStderrNoise('Error: connection refused'), false)
23
+ assert.equal(isStderrNoise('FATAL: segfault'), false)
24
+ assert.equal(isStderrNoise('Permission denied'), false)
25
+ })
26
+ })
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // buildCliEnv
30
+ // ---------------------------------------------------------------------------
31
+
32
+ describe('buildCliEnv', () => {
33
+ it('strips SWARMCLAW_ prefixed vars from env', () => {
34
+ const orig = process.env.SWARMCLAW_TEST_VAR
35
+ process.env.SWARMCLAW_TEST_VAR = 'should_be_stripped'
36
+ try {
37
+ const env = buildCliEnv()
38
+ assert.equal(env.SWARMCLAW_TEST_VAR, undefined)
39
+ } finally {
40
+ if (orig === undefined) delete process.env.SWARMCLAW_TEST_VAR
41
+ else process.env.SWARMCLAW_TEST_VAR = orig
42
+ }
43
+ })
44
+
45
+ it('deletes MallocStackLogging', () => {
46
+ const orig = process.env.MallocStackLogging
47
+ process.env.MallocStackLogging = '1'
48
+ try {
49
+ const env = buildCliEnv()
50
+ assert.equal(env.MallocStackLogging, undefined)
51
+ } finally {
52
+ if (orig === undefined) delete process.env.MallocStackLogging
53
+ else process.env.MallocStackLogging = orig
54
+ }
55
+ })
56
+
57
+ it('injects provided key-value pairs', () => {
58
+ const env = buildCliEnv({ inject: { MY_CUSTOM_KEY: 'hello' } })
59
+ assert.equal(env.MY_CUSTOM_KEY, 'hello')
60
+ })
61
+
62
+ it('preserves unrelated user env vars', () => {
63
+ const env = buildCliEnv()
64
+ assert.equal(env.PATH, process.env.PATH)
65
+ })
66
+
67
+ it('supports custom stripPrefixes', () => {
68
+ const orig = process.env.CUSTOM_PREFIX_VAR
69
+ process.env.CUSTOM_PREFIX_VAR = 'should_be_stripped'
70
+ try {
71
+ const env = buildCliEnv({ stripPrefixes: ['CUSTOM_PREFIX_'] })
72
+ assert.equal(env.CUSTOM_PREFIX_VAR, undefined)
73
+ } finally {
74
+ if (orig === undefined) delete process.env.CUSTOM_PREFIX_VAR
75
+ else process.env.CUSTOM_PREFIX_VAR = orig
76
+ }
77
+ })
78
+
79
+ it('sets TERM=dumb and NO_COLOR=1', () => {
80
+ const env = buildCliEnv()
81
+ assert.equal(env.TERM, 'dumb')
82
+ assert.equal(env.NO_COLOR, '1')
83
+ })
84
+ })
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // isCliProvider
88
+ // ---------------------------------------------------------------------------
89
+
90
+ describe('isCliProvider', () => {
91
+ it('returns true for known CLI providers', () => {
92
+ assert.equal(isCliProvider('claude-cli'), true)
93
+ assert.equal(isCliProvider('codex-cli'), true)
94
+ assert.equal(isCliProvider('opencode-cli'), true)
95
+ assert.equal(isCliProvider('gemini-cli'), true)
96
+ })
97
+
98
+ it('returns false for non-CLI providers', () => {
99
+ assert.equal(isCliProvider('openai'), false)
100
+ assert.equal(isCliProvider('anthropic'), false)
101
+ assert.equal(isCliProvider(''), false)
102
+ assert.equal(isCliProvider('google'), false)
103
+ })
104
+ })
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // CLI_PROVIDER_CAPABILITIES
108
+ // ---------------------------------------------------------------------------
109
+
110
+ describe('CLI_PROVIDER_CAPABILITIES', () => {
111
+ it('has entries for all 4 CLI providers', () => {
112
+ assert.ok('claude-cli' in CLI_PROVIDER_CAPABILITIES)
113
+ assert.ok('codex-cli' in CLI_PROVIDER_CAPABILITIES)
114
+ assert.ok('opencode-cli' in CLI_PROVIDER_CAPABILITIES)
115
+ assert.ok('gemini-cli' in CLI_PROVIDER_CAPABILITIES)
116
+ })
117
+
118
+ it('each entry is a non-empty string', () => {
119
+ for (const [key, value] of Object.entries(CLI_PROVIDER_CAPABILITIES)) {
120
+ assert.equal(typeof value, 'string', `${key} should be a string`)
121
+ assert.ok(value.length > 0, `${key} should be non-empty`)
122
+ }
123
+ })
124
+ })
@@ -106,6 +106,8 @@ export function buildCliEnv(opts?: {
106
106
  }
107
107
  }
108
108
 
109
+ delete (env as Record<string, unknown>).MallocStackLogging
110
+
109
111
  if (opts?.inject) {
110
112
  for (const [key, value] of Object.entries(opts.inject)) {
111
113
  env[key] = value
@@ -115,6 +117,19 @@ export function buildCliEnv(opts?: {
115
117
  return env
116
118
  }
117
119
 
120
+ // ---------------------------------------------------------------------------
121
+ // Stderr Noise Filter
122
+ // ---------------------------------------------------------------------------
123
+
124
+ const STDERR_NOISE_PATTERNS: RegExp[] = [
125
+ /MallocStackLogging/,
126
+ /^\s*$/,
127
+ ]
128
+
129
+ export function isStderrNoise(text: string): boolean {
130
+ return STDERR_NOISE_PATTERNS.some((re) => re.test(text))
131
+ }
132
+
118
133
  // ---------------------------------------------------------------------------
119
134
  // Auth Probing
120
135
  // ---------------------------------------------------------------------------
@@ -5,7 +5,9 @@ import { spawn } from 'child_process'
5
5
  import type { StreamChatOptions } from './index'
6
6
  import { log } from '../server/logger'
7
7
  import { loadRuntimeSettings } from '@/lib/server/runtime/runtime-settings'
8
- import { resolveCliBinary, buildCliEnv, probeCliAuth, attachAbortHandler, symlinkConfigFiles } from './cli-utils'
8
+ import { resolveCliBinary, buildCliEnv, probeCliAuth, attachAbortHandler, symlinkConfigFiles, isStderrNoise } from './cli-utils'
9
+
10
+ const TAG = 'provider-codex'
9
11
 
10
12
  function codexModelRequiresReasoningDowngrade(model: string | null | undefined): boolean {
11
13
  const value = String(model || '').trim().toLowerCase()
@@ -196,8 +198,12 @@ export function streamCodexCliChat({ session, message, imagePath, systemPrompt,
196
198
  const text = chunk.toString()
197
199
  stderrText += text
198
200
  if (stderrText.length > 16_000) stderrText = stderrText.slice(-16_000)
199
- log.warn('codex-cli', `stderr [${session.id}]`, text.slice(0, 500))
200
- console.error(`[${session.id}] codex stderr:`, text.slice(0, 200))
201
+ if (isStderrNoise(text)) {
202
+ log.debug('codex-cli', `stderr noise [${session.id}]`, text.slice(0, 500))
203
+ } else {
204
+ log.warn('codex-cli', `stderr [${session.id}]`, text.slice(0, 500))
205
+ log.error(TAG, `[${session.id}] codex stderr:`, text.slice(0, 200))
206
+ }
201
207
  })
202
208
 
203
209
  return new Promise((resolve) => {
@@ -2,7 +2,7 @@ import { spawn } from 'child_process'
2
2
  import type { StreamChatOptions } from './index'
3
3
  import { log } from '../server/logger'
4
4
  import { loadRuntimeSettings } from '@/lib/server/runtime/runtime-settings'
5
- import { resolveCliBinary, buildCliEnv, probeCliAuth, attachAbortHandler } from './cli-utils'
5
+ import { resolveCliBinary, buildCliEnv, probeCliAuth, attachAbortHandler, isStderrNoise } from './cli-utils'
6
6
 
7
7
  /**
8
8
  * Gemini CLI provider — spawns `gemini --prompt <message> --output-format stream-json --yolo`.
@@ -151,7 +151,11 @@ export function streamGeminiCliChat({ session, message, imagePath, systemPrompt,
151
151
  const text = chunk.toString()
152
152
  stderrText += text
153
153
  if (stderrText.length > 16_000) stderrText = stderrText.slice(-16_000)
154
- log.warn('gemini-cli', `stderr [${session.id}]`, text.slice(0, 500))
154
+ if (isStderrNoise(text)) {
155
+ log.debug('gemini-cli', `stderr noise [${session.id}]`, text.slice(0, 500))
156
+ } else {
157
+ log.warn('gemini-cli', `stderr [${session.id}]`, text.slice(0, 500))
158
+ }
155
159
  })
156
160
 
157
161
  return new Promise((resolve) => {
@@ -8,8 +8,11 @@ import { streamAnthropicChat } from './anthropic'
8
8
  import { streamOpenClawChat } from './openclaw'
9
9
  import { errorMessage, sleep, jitteredBackoff } from '@/lib/shared-utils'
10
10
  import { classifyProviderError } from './error-classification'
11
+ import { log } from '@/lib/server/logger'
11
12
  import type { ProviderInfo, ProviderConfig as CustomProviderConfig, ProviderType } from '../../types'
12
13
 
14
+ const TAG = 'providers'
15
+
13
16
  export interface ProviderHandler {
14
17
  streamChat: (opts: StreamChatOptions) => Promise<string>
15
18
  }
@@ -439,7 +442,7 @@ export async function streamChatWithFailover(
439
442
  if (classified.reason === 'auth_permanent') throw err
440
443
 
441
444
  if (i < credentialIds.length - 1) {
442
- console.log(`[failover] Credential ${credId} failed (${classified.reason}: ${errMessage?.slice(0, 80)}), trying fallback...`)
445
+ log.info(TAG, `Credential ${credId} failed (${classified.reason}: ${errMessage?.slice(0, 80)}), trying fallback...`)
443
446
  opts.write(`data: ${JSON.stringify({
444
447
  t: 'md',
445
448
  text: JSON.stringify({ failover: { from: credId, reason: errMessage?.slice(0, 100) } }),
@@ -3,9 +3,12 @@ import http from 'http'
3
3
  import https from 'https'
4
4
  import type { StreamChatOptions } from './index'
5
5
  import { IMAGE_EXTS, TEXT_EXTS, MAX_HISTORY_MESSAGES, writeSSE } from './provider-defaults'
6
+ import { log } from '@/lib/server/logger'
6
7
  import { resolveOllamaRuntimeConfig } from '@/lib/server/ollama-runtime'
7
8
  import { resolveImagePath } from '@/lib/server/resolve-image'
8
9
 
10
+ const TAG = 'provider-ollama'
11
+
9
12
  export function streamOllamaChat({ session, message, imagePath, apiKey, write, active, loadHistory, onUsage, signal }: StreamChatOptions): Promise<string> {
10
13
  return new Promise((resolve, reject) => {
11
14
  const messages = buildMessages(session, message, imagePath, loadHistory)
@@ -69,7 +72,7 @@ export function streamOllamaChat({ session, message, imagePath, apiKey, write, a
69
72
  apiRes.on('data', (c: Buffer) => errBody += c)
70
73
  apiRes.on('end', () => {
71
74
  const msg = `Ollama error ${apiRes.statusCode}: ${errBody.slice(0, 200)}`
72
- console.error(`[${session.id}] ${msg}`)
75
+ log.error(TAG, `[${session.id}] ${msg}`)
73
76
  writeSSE(write, 'err', msg.slice(0, 120))
74
77
  active.delete(session.id)
75
78
  reject(new Error(msg))
@@ -114,7 +117,7 @@ export function streamOllamaChat({ session, message, imagePath, apiKey, write, a
114
117
  active.set(session.id, { kill: () => { abortController.aborted = true; apiReq.destroy() } })
115
118
 
116
119
  apiReq.on('error', (e: NodeJS.ErrnoException) => {
117
- console.error(`[${session.id}] ollama request error:`, e.message)
120
+ log.error(TAG, `[${session.id}] ollama request error:`, e.message)
118
121
  let errMsg = e.message
119
122
  if (e.code === 'ECONNREFUSED') {
120
123
  errMsg = `Cannot connect to Ollama at ${endpoint}. Is Ollama running?`
@@ -1,8 +1,11 @@
1
1
  import fs from 'fs'
2
2
  import type { StreamChatOptions } from './index'
3
3
  import { PROVIDER_DEFAULTS, IMAGE_EXTS, TEXT_EXTS, PDF_MAX_CHARS, MAX_HISTORY_MESSAGES, writeSSE } from './provider-defaults'
4
+ import { log } from '@/lib/server/logger'
4
5
  import { resolveImagePath } from '@/lib/server/resolve-image'
5
6
 
7
+ const TAG = 'provider-openai'
8
+
6
9
  async function fileToContentParts(filePath: string): Promise<Array<Record<string, unknown>>> {
7
10
  if (!filePath || !fs.existsSync(filePath)) return []
8
11
  const name = filePath.split('/').pop() || 'file'
@@ -108,7 +111,7 @@ export function streamOpenAiChat({ session, message, imagePath, imageUrl, apiKey
108
111
  const resContentType = res.headers.get('content-type') || ''
109
112
  if (resContentType.includes('text/html')) {
110
113
  const msg = 'Received HTML instead of API response. The endpoint may be misconfigured or returning a landing page.'
111
- console.error(`[${session.id}] received HTML instead of API response from ${baseUrl} (provider: ${session.provider})`)
114
+ log.error(TAG, `[${session.id}] received HTML instead of API response from ${baseUrl} (provider: ${session.provider})`)
112
115
  writeSSE(write, 'err', msg)
113
116
  active.delete(session.id)
114
117
  reject(new Error(msg))
@@ -117,7 +120,7 @@ export function streamOpenAiChat({ session, message, imagePath, imageUrl, apiKey
117
120
 
118
121
  if (!res.ok) {
119
122
  const errBody = await res.text().catch(() => '')
120
- console.error(`[${session.id}] openai error ${res.status}:`, errBody.slice(0, 200))
123
+ log.error(TAG, `[${session.id}] openai error ${res.status}:`, errBody.slice(0, 200))
121
124
  let errMsg = `API error (${res.status})`
122
125
  try {
123
126
  const parsed = JSON.parse(errBody)
@@ -133,7 +136,7 @@ export function streamOpenAiChat({ session, message, imagePath, imageUrl, apiKey
133
136
 
134
137
  if (!res.body) {
135
138
  const msg = `No response body from ${baseUrl}`
136
- console.error(`[${session.id}] ${msg}`)
139
+ log.error(TAG, `[${session.id}] ${msg}`)
137
140
  active.delete(session.id)
138
141
  reject(new Error(msg))
139
142
  return
@@ -175,12 +178,12 @@ export function streamOpenAiChat({ session, message, imagePath, imageUrl, apiKey
175
178
  }
176
179
 
177
180
  if (!fullResponse) {
178
- console.error(`[${session.id}] openai stream ended with no content (provider: ${session.provider}, endpoint: ${baseUrl})`)
181
+ log.error(TAG, `[${session.id}] openai stream ended with no content (provider: ${session.provider}, endpoint: ${baseUrl})`)
179
182
  }
180
183
  } catch (err: unknown) {
181
184
  const errObj = err as { name?: string; message?: string }
182
185
  if (errObj.name !== 'AbortError') {
183
- console.error(`[${session.id}] openai request error:`, errObj.message)
186
+ log.error(TAG, `[${session.id}] openai request error:`, errObj.message)
184
187
  writeSSE(write, 'err', `Connection failed: ${errObj.message}`)
185
188
  }
186
189
  active.delete(session.id)
@@ -2,7 +2,7 @@ import { spawn } from 'child_process'
2
2
  import type { StreamChatOptions } from './index'
3
3
  import { log } from '../server/logger'
4
4
  import { loadRuntimeSettings } from '@/lib/server/runtime/runtime-settings'
5
- import { resolveCliBinary, buildCliEnv, probeCliAuth, attachAbortHandler } from './cli-utils'
5
+ import { resolveCliBinary, buildCliEnv, probeCliAuth, attachAbortHandler, isStderrNoise } from './cli-utils'
6
6
 
7
7
  /**
8
8
  * OpenCode CLI provider — spawns `opencode run <message> --format json` for non-interactive usage.
@@ -120,7 +120,11 @@ export function streamOpenCodeCliChat({ session, message, imagePath, systemPromp
120
120
  const text = chunk.toString()
121
121
  stderrText += text
122
122
  if (stderrText.length > 16_000) stderrText = stderrText.slice(-16_000)
123
- log.warn('opencode-cli', `stderr [${session.id}]`, text.slice(0, 500))
123
+ if (isStderrNoise(text)) {
124
+ log.debug('opencode-cli', `stderr noise [${session.id}]`, text.slice(0, 500))
125
+ } else {
126
+ log.warn('opencode-cli', `stderr [${session.id}]`, text.slice(0, 500))
127
+ }
124
128
  })
125
129
 
126
130
  return new Promise((resolve) => {
@@ -0,0 +1,21 @@
1
+ import { loadActivity as loadStoredActivity, logActivity as writeActivityLog } from '@/lib/server/storage'
2
+ import { perf } from '@/lib/server/runtime/perf'
3
+
4
+ export function loadActivity() {
5
+ return perf.measureSync('repository', 'activity.list', () => loadStoredActivity())
6
+ }
7
+
8
+ export function logActivity(entry: {
9
+ entityType: string
10
+ entityId: string
11
+ action: string
12
+ actor: string
13
+ actorId?: string
14
+ summary: string
15
+ detail?: Record<string, unknown>
16
+ }) {
17
+ perf.measureSync('repository', 'activity.log', () => writeActivityLog(entry), {
18
+ entityType: entry.entityType,
19
+ action: entry.action,
20
+ })
21
+ }