@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
@@ -43,7 +43,7 @@ export function streamClaudeCliChat({ session, message, imagePath, systemPrompt,
43
43
  }
44
44
 
45
45
  // Add MCP servers for enabled tools
46
- const tools: string[] = session.tools || []
46
+ const tools: string[] = session.plugins || []
47
47
  let mcpConfigPath: string | null = null
48
48
  if (tools.includes('browser')) {
49
49
  const proxyScript = path.join(process.cwd(), 'src/lib/server/playwright-proxy.mjs')
@@ -0,0 +1,112 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import type { Agent } from '@/types'
4
+ import {
5
+ isDelegationTaskPayload,
6
+ resolveDelegatorAgentId,
7
+ resolveManagedAgentAssignment,
8
+ validateManagedAgentAssignment,
9
+ } from './agent-assignment'
10
+
11
+ const now = Date.now()
12
+ const agents: Record<string, Agent> = {
13
+ molly: {
14
+ id: 'molly',
15
+ name: 'Molly',
16
+ description: '',
17
+ systemPrompt: '',
18
+ provider: 'openai',
19
+ model: 'gpt-4o',
20
+ createdAt: now,
21
+ updatedAt: now,
22
+ },
23
+ writer: {
24
+ id: 'writer',
25
+ name: 'Writer',
26
+ description: '',
27
+ systemPrompt: '',
28
+ provider: 'openai',
29
+ model: 'gpt-4o',
30
+ createdAt: now,
31
+ updatedAt: now,
32
+ },
33
+ }
34
+
35
+ describe('resolveManagedAgentAssignment', () => {
36
+ it('resolves explicit aliases to concrete agent ids', () => {
37
+ const resolved = resolveManagedAgentAssignment({ assignee: 'Writer' }, agents, 'molly')
38
+ assert.equal(resolved.agentId, 'writer')
39
+ assert.equal(resolved.source, 'explicit')
40
+ })
41
+
42
+ it('resolves description-based delegation before scope checks', () => {
43
+ const resolved = resolveManagedAgentAssignment(
44
+ { description: 'Please delegate this to @Writer and let them handle the draft.' },
45
+ agents,
46
+ 'molly',
47
+ )
48
+ assert.equal(resolved.agentId, 'writer')
49
+ assert.equal(resolved.source, 'description')
50
+ })
51
+ })
52
+
53
+ describe('validateManagedAgentAssignment', () => {
54
+ it('blocks assigning another agent when scope is self', () => {
55
+ const resolved = resolveManagedAgentAssignment({ assignee: 'writer' }, agents, 'molly')
56
+ const error = validateManagedAgentAssignment({
57
+ resourceLabel: 'tasks',
58
+ agents,
59
+ assignScope: 'self',
60
+ currentAgentId: 'molly',
61
+ targetAgentId: resolved.agentId,
62
+ unresolvedReference: resolved.unresolvedReference,
63
+ })
64
+ assert.match(error || '', /only assign tasks to yourself/i)
65
+ })
66
+
67
+ it('allows self-assignment in self scope', () => {
68
+ const resolved = resolveManagedAgentAssignment({ agentId: 'molly' }, agents, 'molly')
69
+ const error = validateManagedAgentAssignment({
70
+ resourceLabel: 'tasks',
71
+ agents,
72
+ assignScope: 'self',
73
+ currentAgentId: 'molly',
74
+ targetAgentId: resolved.agentId,
75
+ unresolvedReference: resolved.unresolvedReference,
76
+ })
77
+ assert.equal(error, null)
78
+ })
79
+
80
+ it('rejects unknown explicit agent references', () => {
81
+ const resolved = resolveManagedAgentAssignment({ agentId: 'missing-agent' }, agents, 'molly')
82
+ const error = validateManagedAgentAssignment({
83
+ resourceLabel: 'tasks',
84
+ agents,
85
+ assignScope: 'all',
86
+ currentAgentId: 'molly',
87
+ targetAgentId: resolved.agentId,
88
+ unresolvedReference: resolved.unresolvedReference,
89
+ })
90
+ assert.match(error || '', /unknown agent "missing-agent"/i)
91
+ })
92
+
93
+ it('rejects self-delegation using resolved agent ids', () => {
94
+ const payload = {
95
+ agentId: 'molly',
96
+ sourceType: 'delegation',
97
+ delegatedByAgentId: 'Molly',
98
+ }
99
+ const resolved = resolveManagedAgentAssignment(payload, agents, 'molly')
100
+ const error = validateManagedAgentAssignment({
101
+ resourceLabel: 'tasks',
102
+ agents,
103
+ assignScope: 'all',
104
+ currentAgentId: 'molly',
105
+ targetAgentId: resolved.agentId,
106
+ unresolvedReference: resolved.unresolvedReference,
107
+ isDelegation: isDelegationTaskPayload(payload),
108
+ delegatorAgentId: resolveDelegatorAgentId(payload, agents, 'molly'),
109
+ })
110
+ assert.match(error || '', /different agent id/i)
111
+ })
112
+ })
@@ -0,0 +1,169 @@
1
+ import type { Agent } from '@/types'
2
+ import { resolveAgentReference, resolveTaskAgentFromDescription } from './task-mention'
3
+
4
+ export const MANAGED_AGENT_REFERENCE_KEYS = [
5
+ 'agentId',
6
+ 'agent_id',
7
+ 'assignedAgentId',
8
+ 'assigned_agent_id',
9
+ 'assignedToAgentId',
10
+ 'assigned_to_agent_id',
11
+ 'assigneeId',
12
+ 'assignee_id',
13
+ 'assignedAgent',
14
+ 'assigned_agent',
15
+ 'assignedTo',
16
+ 'assigned_to',
17
+ 'assignee',
18
+ 'agent',
19
+ 'owner',
20
+ ] as const
21
+
22
+ type AssignmentSource = 'explicit' | 'description' | 'fallback' | 'none'
23
+
24
+ export interface ManagedAgentAssignmentResolution {
25
+ agentId: string | null
26
+ explicitReference: string | null
27
+ unresolvedReference: string | null
28
+ source: AssignmentSource
29
+ hadExplicitInput: boolean
30
+ }
31
+
32
+ function firstNonEmptyString(
33
+ parsed: Record<string, unknown>,
34
+ keys: readonly string[],
35
+ ): string | null {
36
+ for (const key of keys) {
37
+ const raw = parsed[key]
38
+ if (typeof raw !== 'string') continue
39
+ const trimmed = raw.trim()
40
+ if (trimmed) return trimmed
41
+ }
42
+ return null
43
+ }
44
+
45
+ export function hasManagedAgentAssignmentInput(
46
+ parsed: Record<string, unknown>,
47
+ keys: readonly string[] = MANAGED_AGENT_REFERENCE_KEYS,
48
+ ): boolean {
49
+ return keys.some((key) => Object.prototype.hasOwnProperty.call(parsed, key))
50
+ }
51
+
52
+ export function resolveManagedAgentAssignment(
53
+ parsed: Record<string, unknown>,
54
+ agents: Record<string, Agent>,
55
+ fallbackAgentId?: string | null,
56
+ opts?: {
57
+ allowDescription?: boolean
58
+ keys?: readonly string[]
59
+ },
60
+ ): ManagedAgentAssignmentResolution {
61
+ const keys = opts?.keys ?? MANAGED_AGENT_REFERENCE_KEYS
62
+ const explicitReference = firstNonEmptyString(parsed, keys)
63
+ const hadExplicitInput = hasManagedAgentAssignmentInput(parsed, keys)
64
+ if (explicitReference) {
65
+ const resolved = resolveAgentReference(explicitReference, agents)
66
+ return {
67
+ agentId: resolved,
68
+ explicitReference,
69
+ unresolvedReference: resolved ? null : explicitReference,
70
+ source: 'explicit',
71
+ hadExplicitInput,
72
+ }
73
+ }
74
+
75
+ if (opts?.allowDescription !== false) {
76
+ const description = typeof parsed.description === 'string' ? parsed.description.trim() : ''
77
+ if (description) {
78
+ const resolvedFromDescription = resolveTaskAgentFromDescription(description, '', agents).trim()
79
+ if (resolvedFromDescription) {
80
+ return {
81
+ agentId: resolvedFromDescription,
82
+ explicitReference: null,
83
+ unresolvedReference: null,
84
+ source: 'description',
85
+ hadExplicitInput,
86
+ }
87
+ }
88
+ }
89
+ }
90
+
91
+ const fallback = typeof fallbackAgentId === 'string' ? fallbackAgentId.trim() : ''
92
+ if (fallback) {
93
+ return {
94
+ agentId: fallback,
95
+ explicitReference: null,
96
+ unresolvedReference: null,
97
+ source: 'fallback',
98
+ hadExplicitInput,
99
+ }
100
+ }
101
+
102
+ return {
103
+ agentId: null,
104
+ explicitReference: null,
105
+ unresolvedReference: null,
106
+ source: 'none',
107
+ hadExplicitInput,
108
+ }
109
+ }
110
+
111
+ export function resolveDelegatorAgentId(
112
+ parsed: Record<string, unknown>,
113
+ agents: Record<string, Agent>,
114
+ fallbackAgentId?: string | null,
115
+ ): string | null {
116
+ const explicitDelegator = typeof parsed.delegatedByAgentId === 'string'
117
+ ? parsed.delegatedByAgentId.trim()
118
+ : ''
119
+ if (explicitDelegator) {
120
+ return resolveAgentReference(explicitDelegator, agents) || explicitDelegator
121
+ }
122
+ const fallback = typeof fallbackAgentId === 'string' ? fallbackAgentId.trim() : ''
123
+ return fallback || null
124
+ }
125
+
126
+ export function isDelegationTaskPayload(parsed: Record<string, unknown>): boolean {
127
+ const sourceType = typeof parsed.sourceType === 'string' ? parsed.sourceType.trim().toLowerCase() : ''
128
+ if (sourceType === 'delegation') return true
129
+ if (typeof parsed.delegatedFromTaskId === 'string' && parsed.delegatedFromTaskId.trim()) return true
130
+ if (typeof parsed.delegatedByAgentId === 'string' && parsed.delegatedByAgentId.trim()) return true
131
+ return false
132
+ }
133
+
134
+ export function validateManagedAgentAssignment(params: {
135
+ resourceLabel: string
136
+ agents: Record<string, Agent>
137
+ assignScope: 'self' | 'all'
138
+ currentAgentId?: string | null
139
+ targetAgentId?: string | null
140
+ unresolvedReference?: string | null
141
+ isDelegation?: boolean
142
+ delegatorAgentId?: string | null
143
+ }): string | null {
144
+ const currentAgentId = typeof params.currentAgentId === 'string' ? params.currentAgentId.trim() : ''
145
+ const targetAgentId = typeof params.targetAgentId === 'string' ? params.targetAgentId.trim() : ''
146
+ const unresolvedReference = typeof params.unresolvedReference === 'string' ? params.unresolvedReference.trim() : ''
147
+ const delegatorAgentId = typeof params.delegatorAgentId === 'string' ? params.delegatorAgentId.trim() : ''
148
+
149
+ if (unresolvedReference) {
150
+ return `Error: Unknown agent "${unresolvedReference}". Use an existing agent ID or exact agent name.`
151
+ }
152
+
153
+ if (targetAgentId && !params.agents[targetAgentId]) {
154
+ return `Error: Unknown agent "${targetAgentId}". Use an existing agent ID or exact agent name.`
155
+ }
156
+
157
+ if (params.assignScope === 'self' && currentAgentId && targetAgentId && targetAgentId !== currentAgentId) {
158
+ return `Error: You can only assign ${params.resourceLabel} to yourself ("${currentAgentId}"). To assign to other agents, ask a user to enable "Assign to Other Agents" in your agent settings.`
159
+ }
160
+
161
+ if (params.isDelegation && targetAgentId) {
162
+ const comparisonId = delegatorAgentId || currentAgentId
163
+ if (comparisonId && targetAgentId === comparisonId) {
164
+ return 'Error: Delegation target must be a different agent ID. Create a normal self-task instead of delegating to yourself.'
165
+ }
166
+ }
167
+
168
+ return null
169
+ }
@@ -0,0 +1,253 @@
1
+ import assert from 'node:assert/strict'
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { spawnSync } from 'node:child_process'
6
+ import { describe, it } from 'node:test'
7
+
8
+ const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../..')
9
+
10
+ function runWithTempDataDir(script: string) {
11
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-approval-connector-'))
12
+ try {
13
+ const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
14
+ cwd: repoRoot,
15
+ env: {
16
+ ...process.env,
17
+ DATA_DIR: tempDir,
18
+ WORKSPACE_DIR: path.join(tempDir, 'workspace'),
19
+ },
20
+ encoding: 'utf-8',
21
+ })
22
+ assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
23
+ const lines = (result.stdout || '')
24
+ .trim()
25
+ .split('\n')
26
+ .map((line) => line.trim())
27
+ .filter(Boolean)
28
+ const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
29
+ return JSON.parse(jsonLine || '{}')
30
+ } finally {
31
+ fs.rmSync(tempDir, { recursive: true, force: true })
32
+ }
33
+ }
34
+
35
+ describe('approval connector reminders', () => {
36
+ it('resolves a due approval to the session connector target and records one-shot delivery state', () => {
37
+ const output = runWithTempDataDir(`
38
+ const storageMod = await import('./src/lib/server/storage.ts')
39
+ const approvalsMod = await import('./src/lib/server/approvals.ts')
40
+ const storage = storageMod.default || storageMod
41
+ const approvals = approvalsMod.default || approvalsMod
42
+
43
+ const now = Date.now()
44
+ storage.saveSettings({
45
+ approvalConnectorNotifyEnabled: true,
46
+ approvalConnectorNotifyDelaySec: 60,
47
+ })
48
+ storage.saveAgents({
49
+ agent_1: {
50
+ id: 'agent_1',
51
+ name: 'Molly',
52
+ description: 'Test agent',
53
+ systemPrompt: 'You are Molly.',
54
+ provider: 'openai',
55
+ model: 'gpt-test',
56
+ plugins: [],
57
+ createdAt: now,
58
+ updatedAt: now,
59
+ },
60
+ })
61
+ storage.saveSessions({
62
+ session_1: {
63
+ id: 'session_1',
64
+ name: 'Connector session',
65
+ cwd: process.cwd(),
66
+ user: 'tester',
67
+ provider: 'openai',
68
+ model: 'gpt-test',
69
+ credentialId: null,
70
+ apiEndpoint: null,
71
+ claudeSessionId: null,
72
+ codexThreadId: null,
73
+ opencodeSessionId: null,
74
+ delegateResumeIds: { claudeCode: null, codex: null, opencode: null, gemini: null },
75
+ messages: [
76
+ {
77
+ role: 'user',
78
+ text: 'Please ask me before spending money.',
79
+ time: now - 1_000,
80
+ source: { connectorId: 'conn-1', channelId: 'chat-42', threadId: 'topic-7' },
81
+ },
82
+ ],
83
+ createdAt: now - 120_000,
84
+ lastActiveAt: now - 1_000,
85
+ sessionType: 'human',
86
+ agentId: 'agent_1',
87
+ plugins: [],
88
+ connectorContext: {
89
+ connectorId: 'conn-1',
90
+ platform: 'telegram',
91
+ channelId: 'chat-42',
92
+ threadId: 'topic-7',
93
+ lastInboundAt: now - 1_000,
94
+ },
95
+ },
96
+ })
97
+
98
+ const approval = approvals.requestApproval({
99
+ category: 'human_loop',
100
+ title: 'Approve plugin install',
101
+ description: 'Need permission to install a plugin.',
102
+ data: {},
103
+ sessionId: 'session_1',
104
+ agentId: 'agent_1',
105
+ })
106
+
107
+ const dueAt = approval.createdAt + 61_000
108
+ const reminders = approvals.listPendingApprovalsNeedingConnectorNotification({
109
+ now: dueAt,
110
+ runningConnectors: [
111
+ { id: 'conn-1', agentId: 'agent_1', supportsSend: true, configuredTargets: [], recentChannelId: 'chat-42' },
112
+ ],
113
+ })
114
+
115
+ approvals.markApprovalConnectorNotificationSent(approval.id, {
116
+ at: dueAt,
117
+ connectorId: 'conn-1',
118
+ channelId: 'chat-42',
119
+ threadId: 'topic-7',
120
+ messageId: 'msg-9',
121
+ })
122
+
123
+ const afterSend = approvals.listPendingApprovalsNeedingConnectorNotification({
124
+ now: dueAt + 1_000,
125
+ runningConnectors: [
126
+ { id: 'conn-1', agentId: 'agent_1', supportsSend: true, configuredTargets: [], recentChannelId: 'chat-42' },
127
+ ],
128
+ })
129
+
130
+ const storedApproval = storage.loadApprovals()[approval.id]
131
+ console.log(JSON.stringify({
132
+ reminderCount: reminders.length,
133
+ reminder: reminders[0],
134
+ afterSendCount: afterSend.length,
135
+ storedApproval,
136
+ }))
137
+ `)
138
+
139
+ assert.equal(output.reminderCount, 1)
140
+ assert.equal(output.reminder.connectorId, 'conn-1')
141
+ assert.equal(output.reminder.channelId, 'chat-42')
142
+ assert.equal(output.reminder.threadId, 'topic-7')
143
+ assert.match(output.reminder.text, /Molly is waiting for your approval/i)
144
+ assert.equal(output.afterSendCount, 0)
145
+ assert.equal(output.storedApproval.connectorNotification.sentAt > 0, true)
146
+ assert.equal(output.storedApproval.connectorNotification.messageId, 'msg-9')
147
+ })
148
+
149
+ it('falls back to a running owned connector and respects retry cooldowns after failed sends', () => {
150
+ const output = runWithTempDataDir(`
151
+ const storageMod = await import('./src/lib/server/storage.ts')
152
+ const approvalsMod = await import('./src/lib/server/approvals.ts')
153
+ const storage = storageMod.default || storageMod
154
+ const approvals = approvalsMod.default || approvalsMod
155
+
156
+ const now = Date.now()
157
+ storage.saveSettings({
158
+ approvalConnectorNotifyEnabled: true,
159
+ approvalConnectorNotifyDelaySec: 30,
160
+ })
161
+ storage.saveAgents({
162
+ agent_2: {
163
+ id: 'agent_2',
164
+ name: 'Writer',
165
+ description: 'Test agent',
166
+ systemPrompt: 'You are Writer.',
167
+ provider: 'openai',
168
+ model: 'gpt-test',
169
+ plugins: [],
170
+ createdAt: now,
171
+ updatedAt: now,
172
+ },
173
+ })
174
+ storage.saveSessions({
175
+ session_plain: {
176
+ id: 'session_plain',
177
+ name: 'Non-connector session',
178
+ cwd: process.cwd(),
179
+ user: 'tester',
180
+ provider: 'openai',
181
+ model: 'gpt-test',
182
+ credentialId: null,
183
+ apiEndpoint: null,
184
+ claudeSessionId: null,
185
+ codexThreadId: null,
186
+ opencodeSessionId: null,
187
+ delegateResumeIds: { claudeCode: null, codex: null, opencode: null, gemini: null },
188
+ messages: [],
189
+ createdAt: now - 60_000,
190
+ lastActiveAt: now - 1_000,
191
+ sessionType: 'human',
192
+ agentId: 'agent_2',
193
+ plugins: [],
194
+ },
195
+ })
196
+
197
+ const approval = approvals.requestApproval({
198
+ category: 'task_tool',
199
+ title: 'Approve outbound outreach',
200
+ description: 'Need your approval before sending a message.',
201
+ data: {},
202
+ sessionId: 'session_plain',
203
+ agentId: 'agent_2',
204
+ })
205
+
206
+ const dueAt = approval.createdAt + 31_000
207
+ const first = approvals.listPendingApprovalsNeedingConnectorNotification({
208
+ now: dueAt,
209
+ runningConnectors: [
210
+ { id: 'conn-fallback', agentId: 'agent_2', supportsSend: true, configuredTargets: [], recentChannelId: 'dm-88' },
211
+ ],
212
+ })
213
+
214
+ approvals.markApprovalConnectorNotificationAttempt(approval.id, {
215
+ at: dueAt,
216
+ connectorId: 'conn-fallback',
217
+ channelId: 'dm-88',
218
+ lastError: 'connector temporarily unavailable',
219
+ })
220
+
221
+ const withinCooldown = approvals.listPendingApprovalsNeedingConnectorNotification({
222
+ now: dueAt + 5_000,
223
+ runningConnectors: [
224
+ { id: 'conn-fallback', agentId: 'agent_2', supportsSend: true, configuredTargets: [], recentChannelId: 'dm-88' },
225
+ ],
226
+ })
227
+
228
+ const afterCooldown = approvals.listPendingApprovalsNeedingConnectorNotification({
229
+ now: dueAt + (10 * 60_000) + 1_000,
230
+ runningConnectors: [
231
+ { id: 'conn-fallback', agentId: 'agent_2', supportsSend: true, configuredTargets: [], recentChannelId: 'dm-88' },
232
+ ],
233
+ })
234
+
235
+ const storedApproval = storage.loadApprovals()[approval.id]
236
+ console.log(JSON.stringify({
237
+ firstCount: first.length,
238
+ fallbackConnectorId: first[0]?.connectorId || null,
239
+ fallbackChannelId: first[0]?.channelId || null,
240
+ withinCooldownCount: withinCooldown.length,
241
+ afterCooldownCount: afterCooldown.length,
242
+ storedApproval,
243
+ }))
244
+ `)
245
+
246
+ assert.equal(output.firstCount, 1)
247
+ assert.equal(output.fallbackConnectorId, 'conn-fallback')
248
+ assert.equal(output.fallbackChannelId, 'dm-88')
249
+ assert.equal(output.withinCooldownCount, 0)
250
+ assert.equal(output.afterCooldownCount, 1)
251
+ assert.equal(output.storedApproval.connectorNotification.lastError, 'connector temporarily unavailable')
252
+ })
253
+ })