@swarmclawai/swarmclaw 0.7.1 → 0.7.3

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 (237) hide show
  1. package/README.md +155 -150
  2. package/package.json +1 -1
  3. package/src/app/api/agents/[id]/route.ts +26 -0
  4. package/src/app/api/agents/[id]/thread/route.ts +37 -9
  5. package/src/app/api/agents/route.ts +13 -2
  6. package/src/app/api/auth/route.ts +76 -7
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
  8. package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
  9. package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
  10. package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
  11. package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
  12. package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
  13. package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
  14. package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
  15. package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
  16. package/src/app/api/{sessions → chats}/route.ts +21 -7
  17. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  18. package/src/app/api/connectors/doctor/route.ts +13 -0
  19. package/src/app/api/files/open/route.ts +16 -14
  20. package/src/app/api/memory/maintenance/route.ts +11 -1
  21. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  22. package/src/app/api/openclaw/skills/route.ts +11 -3
  23. package/src/app/api/plugins/dependencies/route.ts +24 -0
  24. package/src/app/api/plugins/install/route.ts +15 -92
  25. package/src/app/api/plugins/route.ts +6 -26
  26. package/src/app/api/plugins/settings/route.ts +40 -0
  27. package/src/app/api/plugins/ui/route.ts +1 -0
  28. package/src/app/api/settings/route.ts +49 -7
  29. package/src/app/api/tasks/[id]/route.ts +15 -6
  30. package/src/app/api/tasks/bulk/route.ts +2 -2
  31. package/src/app/api/tasks/route.ts +9 -4
  32. package/src/app/api/usage/route.ts +30 -0
  33. package/src/app/api/webhooks/[id]/route.ts +8 -1
  34. package/src/app/page.tsx +9 -2
  35. package/src/cli/index.js +39 -33
  36. package/src/cli/index.ts +43 -49
  37. package/src/cli/spec.js +29 -27
  38. package/src/components/agents/agent-card.tsx +16 -13
  39. package/src/components/agents/agent-chat-list.tsx +104 -4
  40. package/src/components/agents/agent-list.tsx +54 -22
  41. package/src/components/agents/agent-sheet.tsx +209 -18
  42. package/src/components/agents/cron-job-form.tsx +3 -3
  43. package/src/components/agents/inspector-panel.tsx +110 -50
  44. package/src/components/auth/access-key-gate.tsx +36 -97
  45. package/src/components/auth/setup-wizard.tsx +5 -38
  46. package/src/components/chat/chat-area.tsx +39 -27
  47. package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
  48. package/src/components/chat/chat-header.tsx +299 -314
  49. package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
  50. package/src/components/chat/chat-tool-toggles.tsx +26 -17
  51. package/src/components/chat/checkpoint-timeline.tsx +4 -4
  52. package/src/components/chat/message-bubble.tsx +4 -1
  53. package/src/components/chat/message-list.tsx +5 -3
  54. package/src/components/chat/session-debug-panel.tsx +1 -1
  55. package/src/components/chat/tool-request-banner.tsx +3 -3
  56. package/src/components/chatrooms/agent-hover-card.tsx +3 -3
  57. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
  58. package/src/components/chatrooms/chatroom-view.tsx +347 -205
  59. package/src/components/connectors/connector-list.tsx +265 -127
  60. package/src/components/connectors/connector-sheet.tsx +218 -1
  61. package/src/components/home/home-view.tsx +129 -5
  62. package/src/components/layout/app-layout.tsx +392 -182
  63. package/src/components/layout/mobile-header.tsx +26 -8
  64. package/src/components/plugins/plugin-list.tsx +487 -254
  65. package/src/components/plugins/plugin-sheet.tsx +236 -13
  66. package/src/components/projects/project-detail.tsx +183 -0
  67. package/src/components/settings/gateway-connection-panel.tsx +1 -1
  68. package/src/components/shared/agent-picker-list.tsx +2 -2
  69. package/src/components/shared/command-palette.tsx +111 -25
  70. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  71. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  72. package/src/components/shared/settings/section-heartbeat.tsx +78 -1
  73. package/src/components/shared/settings/section-orchestrator.tsx +3 -3
  74. package/src/components/shared/settings/section-providers.tsx +1 -1
  75. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  76. package/src/components/shared/settings/section-secrets.tsx +6 -6
  77. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  78. package/src/components/shared/settings/section-voice.tsx +5 -1
  79. package/src/components/shared/settings/section-web-search.tsx +10 -2
  80. package/src/components/shared/settings/settings-page.tsx +244 -56
  81. package/src/components/tasks/approvals-panel.tsx +205 -18
  82. package/src/components/tasks/task-board.tsx +242 -46
  83. package/src/components/usage/metrics-dashboard.tsx +147 -1
  84. package/src/components/wallets/wallet-panel.tsx +17 -5
  85. package/src/components/webhooks/webhook-sheet.tsx +8 -8
  86. package/src/lib/auth.ts +17 -0
  87. package/src/lib/chat-streaming-state.test.ts +108 -0
  88. package/src/lib/chat-streaming-state.ts +108 -0
  89. package/src/lib/chat.ts +1 -1
  90. package/src/lib/{sessions.ts → chats.ts} +28 -18
  91. package/src/lib/openclaw-agent-id.test.ts +14 -0
  92. package/src/lib/openclaw-agent-id.ts +31 -0
  93. package/src/lib/providers/claude-cli.ts +1 -1
  94. package/src/lib/server/agent-assignment.test.ts +112 -0
  95. package/src/lib/server/agent-assignment.ts +169 -0
  96. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  97. package/src/lib/server/approvals-auto-approve.test.ts +205 -0
  98. package/src/lib/server/approvals.ts +483 -75
  99. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  100. package/src/lib/server/browser-state.test.ts +118 -0
  101. package/src/lib/server/browser-state.ts +123 -0
  102. package/src/lib/server/build-llm.test.ts +36 -0
  103. package/src/lib/server/build-llm.ts +11 -4
  104. package/src/lib/server/builtin-plugins.ts +34 -0
  105. package/src/lib/server/capability-router.ts +10 -8
  106. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  107. package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
  108. package/src/lib/server/chat-execution.ts +285 -165
  109. package/src/lib/server/chatroom-health.test.ts +26 -0
  110. package/src/lib/server/chatroom-health.ts +2 -3
  111. package/src/lib/server/chatroom-helpers.test.ts +67 -2
  112. package/src/lib/server/chatroom-helpers.ts +48 -8
  113. package/src/lib/server/connectors/discord.ts +175 -11
  114. package/src/lib/server/connectors/doctor.test.ts +80 -0
  115. package/src/lib/server/connectors/doctor.ts +116 -0
  116. package/src/lib/server/connectors/manager.ts +948 -112
  117. package/src/lib/server/connectors/policy.test.ts +222 -0
  118. package/src/lib/server/connectors/policy.ts +452 -0
  119. package/src/lib/server/connectors/slack.ts +188 -9
  120. package/src/lib/server/connectors/telegram.ts +65 -15
  121. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  122. package/src/lib/server/connectors/thread-context.ts +72 -0
  123. package/src/lib/server/connectors/types.ts +41 -11
  124. package/src/lib/server/cost.ts +34 -1
  125. package/src/lib/server/daemon-state.ts +61 -3
  126. package/src/lib/server/data-dir.ts +13 -0
  127. package/src/lib/server/delegation-jobs.test.ts +140 -0
  128. package/src/lib/server/delegation-jobs.ts +248 -0
  129. package/src/lib/server/document-utils.test.ts +47 -0
  130. package/src/lib/server/document-utils.ts +397 -0
  131. package/src/lib/server/heartbeat-service.ts +14 -40
  132. package/src/lib/server/heartbeat-source.test.ts +22 -0
  133. package/src/lib/server/heartbeat-source.ts +7 -0
  134. package/src/lib/server/identity-continuity.test.ts +77 -0
  135. package/src/lib/server/identity-continuity.ts +127 -0
  136. package/src/lib/server/mailbox-utils.ts +347 -0
  137. package/src/lib/server/main-agent-loop.ts +28 -1103
  138. package/src/lib/server/memory-db.ts +4 -6
  139. package/src/lib/server/memory-tiers.ts +40 -0
  140. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  141. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  142. package/src/lib/server/openclaw-exec-config.ts +5 -6
  143. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  144. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  145. package/src/lib/server/openclaw-sync.ts +3 -2
  146. package/src/lib/server/orchestrator-lg.ts +20 -9
  147. package/src/lib/server/orchestrator.ts +7 -7
  148. package/src/lib/server/playwright-proxy.mjs +27 -3
  149. package/src/lib/server/plugins.test.ts +207 -0
  150. package/src/lib/server/plugins.ts +927 -66
  151. package/src/lib/server/provider-health.ts +38 -6
  152. package/src/lib/server/queue.ts +13 -28
  153. package/src/lib/server/scheduler.ts +2 -0
  154. package/src/lib/server/session-archive-memory.test.ts +85 -0
  155. package/src/lib/server/session-archive-memory.ts +230 -0
  156. package/src/lib/server/session-mailbox.ts +8 -18
  157. package/src/lib/server/session-reset-policy.test.ts +99 -0
  158. package/src/lib/server/session-reset-policy.ts +311 -0
  159. package/src/lib/server/session-run-manager.ts +33 -82
  160. package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
  161. package/src/lib/server/session-tools/calendar.ts +366 -0
  162. package/src/lib/server/session-tools/canvas.ts +1 -1
  163. package/src/lib/server/session-tools/chatroom.ts +4 -2
  164. package/src/lib/server/session-tools/connector.ts +114 -10
  165. package/src/lib/server/session-tools/context.ts +21 -5
  166. package/src/lib/server/session-tools/crawl.ts +447 -0
  167. package/src/lib/server/session-tools/crud.ts +74 -28
  168. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  169. package/src/lib/server/session-tools/delegate.ts +497 -24
  170. package/src/lib/server/session-tools/discovery.ts +24 -6
  171. package/src/lib/server/session-tools/document.ts +283 -0
  172. package/src/lib/server/session-tools/edit_file.ts +4 -2
  173. package/src/lib/server/session-tools/email.ts +320 -0
  174. package/src/lib/server/session-tools/extract.ts +137 -0
  175. package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
  176. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  177. package/src/lib/server/session-tools/file.ts +241 -25
  178. package/src/lib/server/session-tools/git.ts +1 -1
  179. package/src/lib/server/session-tools/http.ts +1 -1
  180. package/src/lib/server/session-tools/human-loop.ts +227 -0
  181. package/src/lib/server/session-tools/image-gen.ts +380 -0
  182. package/src/lib/server/session-tools/index.ts +130 -50
  183. package/src/lib/server/session-tools/mailbox.ts +276 -0
  184. package/src/lib/server/session-tools/memory.ts +172 -3
  185. package/src/lib/server/session-tools/monitor.ts +151 -8
  186. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  187. package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
  188. package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
  189. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  190. package/src/lib/server/session-tools/platform.ts +148 -7
  191. package/src/lib/server/session-tools/plugin-creator.ts +89 -26
  192. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  193. package/src/lib/server/session-tools/replicate.ts +301 -0
  194. package/src/lib/server/session-tools/sample-ui.ts +1 -1
  195. package/src/lib/server/session-tools/sandbox.ts +4 -2
  196. package/src/lib/server/session-tools/schedule.ts +24 -12
  197. package/src/lib/server/session-tools/session-info.ts +43 -7
  198. package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
  199. package/src/lib/server/session-tools/shell.ts +5 -2
  200. package/src/lib/server/session-tools/subagent.ts +194 -28
  201. package/src/lib/server/session-tools/table.ts +587 -0
  202. package/src/lib/server/session-tools/wallet.ts +42 -12
  203. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  204. package/src/lib/server/session-tools/web.ts +926 -91
  205. package/src/lib/server/storage.ts +255 -16
  206. package/src/lib/server/stream-agent-chat.ts +116 -268
  207. package/src/lib/server/structured-extract.test.ts +72 -0
  208. package/src/lib/server/structured-extract.ts +373 -0
  209. package/src/lib/server/task-mention.test.ts +16 -2
  210. package/src/lib/server/task-mention.ts +61 -10
  211. package/src/lib/server/tool-aliases.ts +66 -18
  212. package/src/lib/server/tool-capability-policy.test.ts +9 -9
  213. package/src/lib/server/tool-capability-policy.ts +38 -27
  214. package/src/lib/server/tool-retry.ts +2 -0
  215. package/src/lib/server/watch-jobs.test.ts +173 -0
  216. package/src/lib/server/watch-jobs.ts +532 -0
  217. package/src/lib/server/ws-hub.ts +5 -3
  218. package/src/lib/tool-definitions.ts +4 -0
  219. package/src/lib/validation/schemas.test.ts +26 -0
  220. package/src/lib/validation/schemas.ts +10 -1
  221. package/src/lib/ws-client.ts +14 -12
  222. package/src/proxy.ts +5 -5
  223. package/src/stores/use-app-store.ts +5 -11
  224. package/src/stores/use-chat-store.ts +38 -9
  225. package/src/types/index.ts +352 -47
  226. package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
  227. package/src/components/sessions/new-session-sheet.tsx +0 -253
  228. package/src/lib/server/main-session.ts +0 -24
  229. package/src/lib/server/session-run-manager.test.ts +0 -23
  230. /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
  231. /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
  232. /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
  233. /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
  234. /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
  235. /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
  236. /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
  237. /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
@@ -0,0 +1,26 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import type { Agent } from '@/types'
4
+ import { filterHealthyChatroomAgents } from './chatroom-health'
5
+
6
+ describe('filterHealthyChatroomAgents', () => {
7
+ it('treats providers with default endpoints as healthy without explicit agent endpoints', () => {
8
+ const now = Date.now()
9
+ const agents: Record<string, Agent> = {
10
+ agent_writer: {
11
+ id: 'agent_writer',
12
+ name: 'Writer',
13
+ description: '',
14
+ systemPrompt: '',
15
+ provider: 'ollama',
16
+ model: 'glm-5:cloud',
17
+ createdAt: now,
18
+ updatedAt: now,
19
+ },
20
+ }
21
+
22
+ const result = filterHealthyChatroomAgents(['agent_writer'], agents)
23
+ assert.deepEqual(result.healthyAgentIds, ['agent_writer'])
24
+ assert.deepEqual(result.skipped, [])
25
+ })
26
+ })
@@ -1,6 +1,6 @@
1
1
  import { getProvider } from '@/lib/providers'
2
2
  import type { Agent } from '@/types'
3
- import { resolveApiKey } from './chatroom-helpers'
3
+ import { resolveAgentApiEndpoint, resolveApiKey } from './chatroom-helpers'
4
4
  import { isProviderCoolingDown } from './provider-health'
5
5
 
6
6
  export interface ChatroomAgentHealthSkip {
@@ -47,7 +47,7 @@ export function filterHealthyChatroomAgents(
47
47
  skipped.push({ agentId, reason: 'missing_api_credentials' })
48
48
  continue
49
49
  }
50
- if (providerInfo.requiresEndpoint && !agent.apiEndpoint) {
50
+ if (providerInfo.requiresEndpoint && !resolveAgentApiEndpoint(agent)) {
51
51
  skipped.push({ agentId, reason: 'missing_api_endpoint' })
52
52
  continue
53
53
  }
@@ -57,4 +57,3 @@ export function filterHealthyChatroomAgents(
57
57
 
58
58
  return { healthyAgentIds, skipped }
59
59
  }
60
-
@@ -1,7 +1,14 @@
1
1
  import { describe, it } from 'node:test'
2
2
  import assert from 'node:assert/strict'
3
3
  import type { Agent, Chatroom } from '@/types'
4
- import { parseMentions, compactChatroomMessages, buildHistoryForAgent } from './chatroom-helpers'
4
+ import {
5
+ parseMentions,
6
+ compactChatroomMessages,
7
+ buildHistoryForAgent,
8
+ buildSyntheticSession,
9
+ resolveAgentApiEndpoint,
10
+ resolveReplyTargetAgentId,
11
+ } from './chatroom-helpers'
5
12
 
6
13
  function makeAgents(): Record<string, Agent> {
7
14
  const now = Date.now()
@@ -37,6 +44,48 @@ describe('chatroom-helpers', () => {
37
44
  assert.deepEqual(mentions, ['default', 'agent_analyst'])
38
45
  })
39
46
 
47
+ it('routes reply-only messages back to the replied-to agent', () => {
48
+ const agents = makeAgents()
49
+ const memberIds = ['default', 'agent_analyst']
50
+ const replyTargetAgentId = resolveReplyTargetAgentId('agent-msg', [
51
+ {
52
+ id: 'agent-msg',
53
+ senderId: 'default',
54
+ senderName: 'Assistant',
55
+ role: 'assistant',
56
+ text: 'Here is the previous answer.',
57
+ mentions: [],
58
+ reactions: [],
59
+ time: Date.now(),
60
+ },
61
+ ], memberIds)
62
+ const mentions = parseMentions('Can you expand on that?', agents, memberIds, { replyTargetAgentId })
63
+ assert.deepEqual(mentions, ['default'])
64
+ })
65
+
66
+ it('keeps explicit mentions ahead of reply-based implicit targeting', () => {
67
+ const agents = makeAgents()
68
+ const memberIds = ['default', 'agent_analyst']
69
+ const mentions = parseMentions('Actually @Analyst should take this one.', agents, memberIds, { replyTargetAgentId: 'default' })
70
+ assert.deepEqual(mentions, ['agent_analyst'])
71
+ })
72
+
73
+ it('ignores replies to non-agent messages', () => {
74
+ const replyTargetAgentId = resolveReplyTargetAgentId('user-msg', [
75
+ {
76
+ id: 'user-msg',
77
+ senderId: 'user',
78
+ senderName: 'You',
79
+ role: 'user',
80
+ text: 'Question',
81
+ mentions: [],
82
+ reactions: [],
83
+ time: Date.now(),
84
+ },
85
+ ], ['default', 'agent_analyst'])
86
+ assert.equal(replyTargetAgentId, null)
87
+ })
88
+
40
89
  it('compacts long chatrooms with a persisted summary message', () => {
41
90
  const now = Date.now()
42
91
  const chatroom: Chatroom = {
@@ -90,5 +139,21 @@ describe('chatroom-helpers', () => {
90
139
  const attachmentMarkers = history.filter((msg) => msg.text.includes('[Attached:')).length
91
140
  assert.ok(attachmentMarkers <= 6)
92
141
  })
93
- })
94
142
 
143
+ it('resolves default provider endpoints for chatroom sessions', () => {
144
+ const now = Date.now()
145
+ const agent: Agent = {
146
+ id: 'agent_writer',
147
+ name: 'Writer',
148
+ description: '',
149
+ systemPrompt: '',
150
+ provider: 'ollama',
151
+ model: 'glm-5:cloud',
152
+ createdAt: now,
153
+ updatedAt: now,
154
+ }
155
+
156
+ assert.equal(resolveAgentApiEndpoint(agent), 'http://localhost:11434')
157
+ assert.equal(buildSyntheticSession(agent, 'room-1').apiEndpoint, 'http://localhost:11434')
158
+ })
159
+ })
@@ -1,8 +1,11 @@
1
1
  import os from 'os'
2
2
  import { loadSettings, loadSkills, loadCredentials, decryptKey } from './storage'
3
3
  import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
4
+ import { buildIdentityContinuityContext } from './identity-continuity'
4
5
  import { genId } from '@/lib/id'
5
- import type { Chatroom, ChatroomMember, Agent, Session, Message } from '@/types'
6
+ import { getProvider } from '@/lib/providers'
7
+ import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
8
+ import type { Chatroom, ChatroomMember, Agent, Session, Message, ChatroomMessage } from '@/types'
6
9
 
7
10
  /** Resolve API key from an agent's credentialId */
8
11
  export function resolveApiKey(credentialId: string | null | undefined): string | null {
@@ -34,6 +37,14 @@ export function isMuted(chatroom: Chatroom, agentId: string): boolean {
34
37
  return new Date(member.mutedUntil).getTime() > Date.now()
35
38
  }
36
39
 
40
+ export function resolveAgentApiEndpoint(agent: Agent): string | null {
41
+ const explicit = normalizeProviderEndpoint(agent.provider, agent.apiEndpoint ?? null)
42
+ if (explicit) return explicit
43
+ const provider = getProvider(agent.provider)
44
+ if (!provider?.defaultEndpoint) return null
45
+ return normalizeProviderEndpoint(agent.provider, provider.defaultEndpoint) || provider.defaultEndpoint.replace(/\/+$/, '')
46
+ }
47
+
37
48
  const COMPACTION_PREFIX = '[Conversation summary]'
38
49
 
39
50
  function normalizeMentionToken(raw: string): string {
@@ -53,7 +64,12 @@ function truncateText(text: string, max: number): string {
53
64
  import { isImplicitlyMentioned } from './chatroom-orchestration'
54
65
 
55
66
  /** Parse @mentions from message text, returns matching agentIds */
56
- export function parseMentions(text: string, agents: Record<string, Agent>, memberIds: string[]): string[] {
67
+ export function parseMentions(
68
+ text: string,
69
+ agents: Record<string, Agent>,
70
+ memberIds: string[],
71
+ opts?: { replyTargetAgentId?: string | null },
72
+ ): string[] {
57
73
  if (/@all\b/i.test(text)) return [...memberIds]
58
74
  const mentionPattern = /(?:^|[\s(])@([a-zA-Z0-9._-]+)/g
59
75
  const mentioned: string[] = []
@@ -73,8 +89,17 @@ export function parseMentions(text: string, agents: Record<string, Agent>, membe
73
89
  }
74
90
  }
75
91
 
76
- // 2. Implicit mentions (OpenClaw Style - Reading the room)
77
- // Only if no explicit mentions found yet
92
+ // 2. Reply-based implicit mention
93
+ // Only if no explicit mentions were found.
94
+ if (mentioned.length === 0) {
95
+ const replyTargetAgentId = opts?.replyTargetAgentId
96
+ if (replyTargetAgentId && memberIds.includes(replyTargetAgentId)) {
97
+ mentioned.push(replyTargetAgentId)
98
+ }
99
+ }
100
+
101
+ // 3. Implicit mentions (OpenClaw Style - Reading the room)
102
+ // Only if no explicit mentions were found.
78
103
  if (mentioned.length === 0) {
79
104
  for (const id of memberIds) {
80
105
  const agent = agents[id]
@@ -87,6 +112,19 @@ export function parseMentions(text: string, agents: Record<string, Agent>, membe
87
112
  return mentioned
88
113
  }
89
114
 
115
+ export function resolveReplyTargetAgentId(
116
+ replyToId: string | undefined,
117
+ messages: ChatroomMessage[],
118
+ memberIds: string[],
119
+ ): string | null {
120
+ if (!replyToId) return null
121
+ const replyMsg = messages.find((m) => m.id === replyToId)
122
+ if (!replyMsg) return null
123
+ if (replyMsg.role !== 'assistant') return null
124
+ if (!memberIds.includes(replyMsg.senderId)) return null
125
+ return replyMsg.senderId
126
+ }
127
+
90
128
  /**
91
129
  * Persisted chatroom compaction so long-lived rooms stay inside context budgets.
92
130
  * Returns true when the message list was compacted.
@@ -126,9 +164,9 @@ export function buildChatroomSystemPrompt(chatroom: Chatroom, agents: Record<str
126
164
  .map((id) => {
127
165
  const a = agents[id]
128
166
  if (!a) return null
129
- const tools = a.tools?.length ? `Tools: ${a.tools.join(', ')}` : 'No specialized tools'
167
+ const plugins = (a.plugins || a.tools)?.length ? `Plugins: ${(a.plugins || a.tools)!.join(', ')}` : 'No specialized plugins'
130
168
  const desc = a.description || a.soul || 'No description'
131
- return `- **${a.name}**: ${desc}\n ${tools}`
169
+ return `- **${a.name}**: ${desc}\n ${plugins}`
132
170
  })
133
171
  .filter(Boolean)
134
172
  .join('\n')
@@ -182,12 +220,12 @@ export function buildSyntheticSession(agent: Agent, chatroomId: string): Session
182
220
  model: agent.model,
183
221
  credentialId: agent.credentialId ?? null,
184
222
  fallbackCredentialIds: agent.fallbackCredentialIds,
185
- apiEndpoint: agent.apiEndpoint ?? null,
223
+ apiEndpoint: resolveAgentApiEndpoint(agent),
186
224
  claudeSessionId: null,
187
225
  messages: [],
188
226
  createdAt: Date.now(),
189
227
  lastActiveAt: Date.now(),
190
- tools: agent.tools || [],
228
+ plugins: agent.plugins || agent.tools || [],
191
229
  agentId: agent.id,
192
230
  }
193
231
  }
@@ -207,6 +245,8 @@ export function buildAgentSystemPromptForChatroom(agent: Agent): string {
207
245
  if (agent.description) identityLines.push(`Description: ${agent.description}`)
208
246
  identityLines.push('I should always embody this identity in the group chat. I am a persistent companion with my own soul and presence.')
209
247
  parts.push(identityLines.join('\n'))
248
+ const continuityBlock = buildIdentityContinuityContext(null, agent)
249
+ if (continuityBlock) parts.push(continuityBlock)
210
250
 
211
251
  // 2. Runtime & Capabilities (OpenClaw Style)
212
252
  const runtimeLines = [
@@ -2,9 +2,94 @@ import { Client, GatewayIntentBits, Events, Partials, AttachmentBuilder } from '
2
2
  import fs from 'fs'
3
3
  import path from 'path'
4
4
  import type { Connector } from '@/types'
5
- import type { PlatformConnector, ConnectorInstance, InboundMessage } from './types'
5
+ import type { PlatformConnector, ConnectorInstance, InboundMessage, InboundThreadHistoryEntry } from './types'
6
6
  import { downloadInboundMediaToUpload, inferInboundMediaType } from './media'
7
- import { isNoMessage } from './manager'
7
+ import { getConnectorReplySendOptions, isNoMessage, recordConnectorOutboundDelivery } from './manager'
8
+
9
+ function buildDiscordThreadTitle(params: {
10
+ threadName?: string
11
+ channelName?: string
12
+ starterText?: string
13
+ fallbackId: string
14
+ }): string {
15
+ const threadName = String(params.threadName || '').trim()
16
+ if (threadName) return threadName
17
+ const snippet = String(params.starterText || '').replace(/\s+/g, ' ').trim().slice(0, 56)
18
+ if (snippet) return `${params.channelName || 'Discord'} · ${snippet}`
19
+ return `${params.channelName || 'Discord'} thread ${params.fallbackId}`
20
+ }
21
+
22
+ function discordSenderName(message: any): string {
23
+ return message?.member?.displayName
24
+ || message?.author?.globalName
25
+ || message?.author?.displayName
26
+ || message?.author?.username
27
+ || 'Unknown'
28
+ }
29
+
30
+ async function hydrateDiscordThreadContext(message: any, inbound: InboundMessage): Promise<void> {
31
+ const channel = message.channel as any
32
+ const isThread = typeof channel?.isThread === 'function' && channel.isThread()
33
+
34
+ try {
35
+ if (isThread) {
36
+ const starter = typeof channel.fetchStarterMessage === 'function'
37
+ ? await channel.fetchStarterMessage().catch(() => null)
38
+ : null
39
+ const historyCollection = channel?.messages && typeof channel.messages.fetch === 'function'
40
+ ? await channel.messages.fetch({ limit: 8, before: message.id }).catch(() => null)
41
+ : null
42
+ const historyMessages = historyCollection
43
+ ? Array.from(historyCollection.values()).sort((a: any, b: any) => (a.createdTimestamp || 0) - (b.createdTimestamp || 0))
44
+ : []
45
+ const history: InboundThreadHistoryEntry[] = historyMessages
46
+ .filter((item: any) => item?.content?.trim())
47
+ .map((item: any) => ({
48
+ role: (item.author?.bot ? 'assistant' : 'user') as 'assistant' | 'user',
49
+ senderName: discordSenderName(item),
50
+ text: item.content,
51
+ messageId: item.id,
52
+ }))
53
+
54
+ inbound.threadParentChannelId = channel?.parentId || undefined
55
+ inbound.threadParentChannelName = channel?.parent?.name || undefined
56
+ inbound.threadStarterText = starter?.content?.trim() || undefined
57
+ inbound.threadStarterSenderName = starter ? discordSenderName(starter) : undefined
58
+ inbound.threadTitle = buildDiscordThreadTitle({
59
+ threadName: channel?.name,
60
+ channelName: inbound.threadParentChannelName || inbound.channelName,
61
+ starterText: inbound.threadStarterText,
62
+ fallbackId: inbound.threadId || inbound.channelId,
63
+ })
64
+ inbound.threadPersonaLabel = inbound.threadTitle
65
+ inbound.threadHistory = history.length ? history : undefined
66
+ return
67
+ }
68
+
69
+ if (message.reference?.messageId && typeof message.fetchReference === 'function') {
70
+ const starter = await message.fetchReference().catch(() => null)
71
+ if (!starter) return
72
+ inbound.threadStarterText = starter.content?.trim() || undefined
73
+ inbound.threadStarterSenderName = discordSenderName(starter)
74
+ inbound.threadParentChannelId = inbound.channelId
75
+ inbound.threadParentChannelName = inbound.channelName
76
+ inbound.threadTitle = buildDiscordThreadTitle({
77
+ channelName: inbound.channelName,
78
+ starterText: inbound.threadStarterText,
79
+ fallbackId: starter.id || inbound.replyToMessageId || inbound.channelId,
80
+ })
81
+ inbound.threadPersonaLabel = inbound.threadTitle
82
+ inbound.threadHistory = [{
83
+ role: (starter.author?.bot ? 'assistant' : 'user') as 'assistant' | 'user',
84
+ senderName: discordSenderName(starter),
85
+ text: starter.content || '',
86
+ messageId: starter.id,
87
+ }].filter((entry) => entry.text.trim().length > 0)
88
+ }
89
+ } catch (err: unknown) {
90
+ console.warn(`[discord] Thread context bootstrap failed: ${err instanceof Error ? err.message : String(err)}`)
91
+ }
92
+ }
8
93
 
9
94
  const discord: PlatformConnector = {
10
95
  async start(connector, botToken, onMessage): Promise<ConnectorInstance> {
@@ -23,6 +108,23 @@ const discord: PlatformConnector = {
23
108
  ? connector.config.channelIds.split(',').map((s) => s.trim()).filter(Boolean)
24
109
  : null
25
110
 
111
+ async function resolveTextChannel(targetChannelId: string) {
112
+ const channel = await client.channels.fetch(targetChannelId)
113
+ if (!channel || !('send' in channel) || typeof (channel as any).send !== 'function') {
114
+ throw new Error(`Cannot send to channel ${targetChannelId}`)
115
+ }
116
+ return channel as any
117
+ }
118
+
119
+ async function resolveChannelMessage(targetChannelId: string, messageId: string) {
120
+ const channel = await resolveTextChannel(targetChannelId)
121
+ const messages = (channel as any).messages
122
+ if (!messages || typeof messages.fetch !== 'function') {
123
+ throw new Error(`Channel ${targetChannelId} does not support message actions`)
124
+ }
125
+ return await messages.fetch(messageId)
126
+ }
127
+
26
128
  client.on(Events.MessageCreate, async (message) => {
27
129
  console.log(`[discord] Message from ${message.author.username} in ${message.channel.type === 1 ? 'DM' : '#' + ('name' in message.channel ? (message.channel as any).name : message.channelId)}: ${message.content.slice(0, 80)}`)
28
130
  // Ignore bot messages
@@ -71,9 +173,17 @@ const discord: PlatformConnector = {
71
173
  senderId: message.author.id,
72
174
  senderName: message.author.displayName || message.author.username,
73
175
  text: message.content || (media.length > 0 ? '(media message)' : ''),
176
+ isGroup: message.channel.type !== 1,
177
+ messageId: message.id,
178
+ mentionsBot: client.user ? message.mentions.users.has(client.user.id) : false,
74
179
  imageUrl: firstImage?.url,
75
180
  media,
181
+ replyToMessageId: message.reference?.messageId || undefined,
182
+ threadId: typeof (message.channel as any).isThread === 'function' && (message.channel as any).isThread()
183
+ ? message.channelId
184
+ : undefined,
76
185
  }
186
+ await hydrateDiscordThreadContext(message, inbound)
77
187
 
78
188
  try {
79
189
  // Show typing indicator
@@ -82,16 +192,41 @@ const discord: PlatformConnector = {
82
192
 
83
193
  if (isNoMessage(response)) return
84
194
 
195
+ const replyOptions = getConnectorReplySendOptions({ connectorId: connector.id, inbound })
196
+ const targetChannelId = replyOptions.threadId || inbound.channelId
197
+ const sendChunk = async (chunk: string, isFirstChunk: boolean) => {
198
+ const channel = await resolveTextChannel(targetChannelId)
199
+ const payload: Record<string, unknown> = {
200
+ content: chunk,
201
+ allowedMentions: { repliedUser: false },
202
+ }
203
+ if (isFirstChunk && replyOptions.replyToMessageId) {
204
+ payload.reply = {
205
+ messageReference: replyOptions.replyToMessageId,
206
+ failIfNotExists: false,
207
+ }
208
+ }
209
+ const sent = await channel.send(payload)
210
+ return String(sent.id || '')
211
+ }
212
+
213
+ let lastMessageId: string | undefined
85
214
  // Discord has a 2000 char limit per message
86
215
  if (response.length <= 2000) {
87
- await message.channel.send(response)
216
+ lastMessageId = await sendChunk(response, true)
88
217
  } else {
89
218
  // Split into chunks
90
219
  const chunks = response.match(/[\s\S]{1,1990}/g) || [response]
91
- for (const chunk of chunks) {
92
- await message.channel.send(chunk)
220
+ for (let i = 0; i < chunks.length; i += 1) {
221
+ lastMessageId = await sendChunk(chunks[i], i === 0)
93
222
  }
94
223
  }
224
+ await recordConnectorOutboundDelivery({
225
+ connectorId: connector.id,
226
+ inbound,
227
+ messageId: lastMessageId,
228
+ state: 'sent',
229
+ })
95
230
  } catch (err: any) {
96
231
  console.error(`[discord] Error handling message:`, err.message)
97
232
  try {
@@ -109,10 +244,8 @@ const discord: PlatformConnector = {
109
244
  return client.isReady()
110
245
  },
111
246
  async sendMessage(channelId, text, options) {
112
- const channel = await client.channels.fetch(channelId)
113
- if (!channel || !('send' in channel) || typeof (channel as any).send !== 'function') {
114
- throw new Error(`Cannot send to channel ${channelId}`)
115
- }
247
+ const targetChannelId = options?.threadId?.trim() || channelId
248
+ const channel = await resolveTextChannel(targetChannelId)
116
249
 
117
250
  const files: AttachmentBuilder[] = []
118
251
  if (options?.mediaPath) {
@@ -125,12 +258,43 @@ const discord: PlatformConnector = {
125
258
  }
126
259
 
127
260
  const content = options?.caption || text || undefined
128
- const msg = await (channel as any).send({
261
+ const payload: Record<string, unknown> = {
129
262
  content: content || (files.length ? undefined : '(empty)'),
130
263
  files: files.length ? files : undefined,
131
- })
264
+ }
265
+ if (options?.replyToMessageId) {
266
+ payload.reply = {
267
+ messageReference: options.replyToMessageId,
268
+ failIfNotExists: false,
269
+ }
270
+ }
271
+
272
+ const msg = await channel.send(payload)
132
273
  return { messageId: msg.id }
133
274
  },
275
+ async sendReaction(channelId, messageId, emoji) {
276
+ const message = await resolveChannelMessage(channelId, messageId)
277
+ await message.react(emoji)
278
+ },
279
+ async editMessage(channelId, messageId, newText) {
280
+ const message = await resolveChannelMessage(channelId, messageId)
281
+ await message.edit(newText)
282
+ },
283
+ async deleteMessage(channelId, messageId) {
284
+ const message = await resolveChannelMessage(channelId, messageId)
285
+ await message.delete()
286
+ },
287
+ async pinMessage(channelId, messageId) {
288
+ const message = await resolveChannelMessage(channelId, messageId)
289
+ await message.pin()
290
+ },
291
+ async sendTyping(channelId, options) {
292
+ const targetChannelId = options?.threadId?.trim() || channelId
293
+ const channel = await resolveTextChannel(targetChannelId)
294
+ if (typeof channel.sendTyping === 'function') {
295
+ await channel.sendTyping()
296
+ }
297
+ },
134
298
  async stop() {
135
299
  client.destroy()
136
300
  console.log(`[discord] Bot disconnected`)
@@ -0,0 +1,80 @@
1
+ import assert from 'node:assert/strict'
2
+ import test from 'node:test'
3
+ import type { Connector } from '@/types'
4
+ import { buildConnectorDoctorPreview, buildConnectorDoctorReport } from './doctor'
5
+
6
+ test('buildConnectorDoctorPreview merges overrides onto an existing connector', () => {
7
+ const base: Connector = {
8
+ id: 'connector-1',
9
+ name: 'Existing Connector',
10
+ platform: 'slack',
11
+ agentId: 'agent-1',
12
+ chatroomId: null,
13
+ credentialId: 'cred-1',
14
+ config: { replyMode: 'first', threadBinding: 'prefer' },
15
+ isEnabled: true,
16
+ status: 'running',
17
+ createdAt: 1,
18
+ updatedAt: 1,
19
+ }
20
+
21
+ const preview = buildConnectorDoctorPreview({
22
+ baseConnector: base,
23
+ input: {
24
+ name: 'Preview Connector',
25
+ agentId: null,
26
+ chatroomId: 'chatroom-9',
27
+ config: { replyMode: 'off', sessionScope: 'main' },
28
+ },
29
+ })
30
+
31
+ assert.equal(preview.name, 'Preview Connector')
32
+ assert.equal(preview.agentId, null)
33
+ assert.equal(preview.chatroomId, 'chatroom-9')
34
+ assert.deepEqual(preview.config, { replyMode: 'off', sessionScope: 'main' })
35
+ })
36
+
37
+ test('buildConnectorDoctorReport returns effective warnings and policy for preview connectors', () => {
38
+ const connector = buildConnectorDoctorPreview({
39
+ input: {
40
+ platform: 'telegram',
41
+ config: {
42
+ sessionScope: 'main',
43
+ replyMode: 'off',
44
+ threadBinding: 'off',
45
+ idleTimeoutSec: '0',
46
+ maxAgeSec: '0',
47
+ },
48
+ },
49
+ })
50
+
51
+ const report = buildConnectorDoctorReport(connector)
52
+
53
+ assert.equal(report.policy.scope, 'main')
54
+ assert.equal(report.policy.replyMode, 'off')
55
+ assert.ok(report.warnings.some((item) => item.includes('blend unrelated connector conversations')))
56
+ assert.ok(report.warnings.some((item) => item.includes('freshness reset is disabled')))
57
+ })
58
+
59
+ test('buildConnectorDoctorReport includes runtime warning for stopped existing connectors', () => {
60
+ const connector = buildConnectorDoctorPreview({
61
+ baseConnector: {
62
+ id: 'connector-2',
63
+ name: 'Existing Connector',
64
+ platform: 'slack',
65
+ agentId: 'agent-1',
66
+ chatroomId: null,
67
+ credentialId: 'cred-1',
68
+ config: {},
69
+ isEnabled: true,
70
+ status: 'stopped',
71
+ createdAt: 1,
72
+ updatedAt: 1,
73
+ },
74
+ input: {},
75
+ })
76
+
77
+ const report = buildConnectorDoctorReport(connector, null, { baseConnector: connector })
78
+
79
+ assert.ok(report.warnings.some((item) => item.includes('not currently connected')))
80
+ })