@swarmclawai/swarmclaw 0.7.2 → 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 (197) hide show
  1. package/README.md +81 -22
  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 +36 -7
  5. package/src/app/api/agents/route.ts +12 -1
  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/chats/[id]/browser/route.ts +5 -1
  9. package/src/app/api/chats/[id]/chat/route.ts +7 -3
  10. package/src/app/api/chats/[id]/main-loop/route.ts +7 -88
  11. package/src/app/api/chats/[id]/messages/route.ts +19 -13
  12. package/src/app/api/chats/[id]/route.ts +18 -0
  13. package/src/app/api/chats/[id]/stop/route.ts +6 -1
  14. package/src/app/api/chats/route.ts +16 -0
  15. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  16. package/src/app/api/connectors/doctor/route.ts +13 -0
  17. package/src/app/api/files/open/route.ts +16 -14
  18. package/src/app/api/memory/maintenance/route.ts +11 -1
  19. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  20. package/src/app/api/openclaw/skills/route.ts +11 -3
  21. package/src/app/api/plugins/dependencies/route.ts +24 -0
  22. package/src/app/api/plugins/install/route.ts +15 -92
  23. package/src/app/api/plugins/route.ts +3 -26
  24. package/src/app/api/plugins/settings/route.ts +17 -12
  25. package/src/app/api/plugins/ui/route.ts +1 -0
  26. package/src/app/api/settings/route.ts +49 -7
  27. package/src/app/api/tasks/[id]/route.ts +15 -6
  28. package/src/app/api/tasks/bulk/route.ts +2 -2
  29. package/src/app/api/tasks/route.ts +9 -4
  30. package/src/app/api/webhooks/[id]/route.ts +8 -1
  31. package/src/app/page.tsx +9 -2
  32. package/src/cli/index.js +4 -0
  33. package/src/cli/index.ts +3 -10
  34. package/src/components/agents/agent-card.tsx +15 -12
  35. package/src/components/agents/agent-chat-list.tsx +101 -1
  36. package/src/components/agents/agent-list.tsx +46 -9
  37. package/src/components/agents/agent-sheet.tsx +207 -16
  38. package/src/components/agents/inspector-panel.tsx +108 -48
  39. package/src/components/auth/access-key-gate.tsx +36 -97
  40. package/src/components/chat/chat-area.tsx +29 -13
  41. package/src/components/chat/chat-card.tsx +4 -20
  42. package/src/components/chat/chat-header.tsx +255 -353
  43. package/src/components/chat/chat-list.tsx +7 -9
  44. package/src/components/chat/checkpoint-timeline.tsx +1 -1
  45. package/src/components/chat/message-list.tsx +3 -1
  46. package/src/components/chatrooms/chatroom-view.tsx +347 -205
  47. package/src/components/connectors/connector-list.tsx +265 -127
  48. package/src/components/connectors/connector-sheet.tsx +217 -0
  49. package/src/components/home/home-view.tsx +128 -4
  50. package/src/components/layout/app-layout.tsx +383 -194
  51. package/src/components/layout/mobile-header.tsx +26 -8
  52. package/src/components/plugins/plugin-list.tsx +15 -3
  53. package/src/components/plugins/plugin-sheet.tsx +118 -9
  54. package/src/components/projects/project-detail.tsx +183 -0
  55. package/src/components/shared/agent-picker-list.tsx +2 -2
  56. package/src/components/shared/command-palette.tsx +111 -24
  57. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  58. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  59. package/src/components/shared/settings/section-heartbeat.tsx +77 -0
  60. package/src/components/shared/settings/section-orchestrator.tsx +3 -3
  61. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  62. package/src/components/shared/settings/section-secrets.tsx +6 -6
  63. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  64. package/src/components/shared/settings/section-voice.tsx +5 -1
  65. package/src/components/shared/settings/section-web-search.tsx +10 -2
  66. package/src/components/shared/settings/settings-page.tsx +245 -46
  67. package/src/components/tasks/approvals-panel.tsx +205 -18
  68. package/src/components/tasks/task-board.tsx +242 -46
  69. package/src/components/usage/metrics-dashboard.tsx +74 -1
  70. package/src/components/wallets/wallet-panel.tsx +17 -5
  71. package/src/components/webhooks/webhook-sheet.tsx +7 -7
  72. package/src/lib/auth.ts +17 -0
  73. package/src/lib/chat-streaming-state.test.ts +108 -0
  74. package/src/lib/chat-streaming-state.ts +108 -0
  75. package/src/lib/openclaw-agent-id.test.ts +14 -0
  76. package/src/lib/openclaw-agent-id.ts +31 -0
  77. package/src/lib/server/agent-assignment.test.ts +112 -0
  78. package/src/lib/server/agent-assignment.ts +169 -0
  79. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  80. package/src/lib/server/approvals-auto-approve.test.ts +205 -0
  81. package/src/lib/server/approvals.ts +483 -75
  82. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  83. package/src/lib/server/browser-state.test.ts +118 -0
  84. package/src/lib/server/browser-state.ts +123 -0
  85. package/src/lib/server/build-llm.test.ts +36 -0
  86. package/src/lib/server/build-llm.ts +11 -4
  87. package/src/lib/server/builtin-plugins.ts +34 -0
  88. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  89. package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
  90. package/src/lib/server/chat-execution.ts +250 -61
  91. package/src/lib/server/chatroom-health.test.ts +26 -0
  92. package/src/lib/server/chatroom-health.ts +2 -3
  93. package/src/lib/server/chatroom-helpers.test.ts +67 -2
  94. package/src/lib/server/chatroom-helpers.ts +45 -5
  95. package/src/lib/server/connectors/discord.ts +175 -11
  96. package/src/lib/server/connectors/doctor.test.ts +80 -0
  97. package/src/lib/server/connectors/doctor.ts +116 -0
  98. package/src/lib/server/connectors/manager.ts +946 -110
  99. package/src/lib/server/connectors/policy.test.ts +222 -0
  100. package/src/lib/server/connectors/policy.ts +452 -0
  101. package/src/lib/server/connectors/slack.ts +188 -9
  102. package/src/lib/server/connectors/telegram.ts +65 -15
  103. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  104. package/src/lib/server/connectors/thread-context.ts +72 -0
  105. package/src/lib/server/connectors/types.ts +41 -11
  106. package/src/lib/server/daemon-state.ts +59 -1
  107. package/src/lib/server/data-dir.ts +13 -0
  108. package/src/lib/server/delegation-jobs.test.ts +140 -0
  109. package/src/lib/server/delegation-jobs.ts +248 -0
  110. package/src/lib/server/document-utils.test.ts +47 -0
  111. package/src/lib/server/document-utils.ts +397 -0
  112. package/src/lib/server/heartbeat-service.ts +13 -39
  113. package/src/lib/server/heartbeat-source.test.ts +22 -0
  114. package/src/lib/server/heartbeat-source.ts +7 -0
  115. package/src/lib/server/identity-continuity.test.ts +77 -0
  116. package/src/lib/server/identity-continuity.ts +127 -0
  117. package/src/lib/server/mailbox-utils.ts +347 -0
  118. package/src/lib/server/main-agent-loop.ts +27 -967
  119. package/src/lib/server/memory-db.ts +4 -6
  120. package/src/lib/server/memory-tiers.ts +40 -0
  121. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  122. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  123. package/src/lib/server/openclaw-exec-config.ts +5 -6
  124. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  125. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  126. package/src/lib/server/openclaw-sync.ts +3 -2
  127. package/src/lib/server/orchestrator-lg.ts +17 -6
  128. package/src/lib/server/orchestrator.ts +2 -2
  129. package/src/lib/server/playwright-proxy.mjs +27 -3
  130. package/src/lib/server/plugins.test.ts +207 -0
  131. package/src/lib/server/plugins.ts +822 -69
  132. package/src/lib/server/provider-health.ts +33 -3
  133. package/src/lib/server/queue.ts +3 -20
  134. package/src/lib/server/scheduler.ts +2 -0
  135. package/src/lib/server/session-archive-memory.test.ts +85 -0
  136. package/src/lib/server/session-archive-memory.ts +230 -0
  137. package/src/lib/server/session-mailbox.ts +8 -18
  138. package/src/lib/server/session-reset-policy.test.ts +99 -0
  139. package/src/lib/server/session-reset-policy.ts +311 -0
  140. package/src/lib/server/session-run-manager.ts +33 -80
  141. package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
  142. package/src/lib/server/session-tools/calendar.ts +2 -12
  143. package/src/lib/server/session-tools/connector.ts +109 -8
  144. package/src/lib/server/session-tools/context.ts +14 -2
  145. package/src/lib/server/session-tools/crawl.ts +447 -0
  146. package/src/lib/server/session-tools/crud.ts +70 -32
  147. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  148. package/src/lib/server/session-tools/delegate.ts +406 -20
  149. package/src/lib/server/session-tools/discovery.ts +22 -4
  150. package/src/lib/server/session-tools/document.ts +283 -0
  151. package/src/lib/server/session-tools/email.ts +1 -3
  152. package/src/lib/server/session-tools/extract.ts +137 -0
  153. package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
  154. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  155. package/src/lib/server/session-tools/file.ts +237 -24
  156. package/src/lib/server/session-tools/human-loop.ts +227 -0
  157. package/src/lib/server/session-tools/image-gen.ts +1 -3
  158. package/src/lib/server/session-tools/index.ts +56 -1
  159. package/src/lib/server/session-tools/mailbox.ts +276 -0
  160. package/src/lib/server/session-tools/memory.ts +35 -3
  161. package/src/lib/server/session-tools/monitor.ts +150 -7
  162. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  163. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  164. package/src/lib/server/session-tools/platform.ts +142 -4
  165. package/src/lib/server/session-tools/plugin-creator.ts +86 -23
  166. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  167. package/src/lib/server/session-tools/replicate.ts +1 -3
  168. package/src/lib/server/session-tools/schedule.ts +20 -10
  169. package/src/lib/server/session-tools/session-info.ts +36 -3
  170. package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
  171. package/src/lib/server/session-tools/subagent.ts +193 -27
  172. package/src/lib/server/session-tools/table.ts +587 -0
  173. package/src/lib/server/session-tools/wallet.ts +13 -10
  174. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  175. package/src/lib/server/session-tools/web.ts +896 -100
  176. package/src/lib/server/storage.ts +226 -7
  177. package/src/lib/server/stream-agent-chat.ts +46 -21
  178. package/src/lib/server/structured-extract.test.ts +72 -0
  179. package/src/lib/server/structured-extract.ts +373 -0
  180. package/src/lib/server/task-mention.test.ts +16 -2
  181. package/src/lib/server/task-mention.ts +61 -10
  182. package/src/lib/server/tool-aliases.ts +44 -7
  183. package/src/lib/server/tool-capability-policy.ts +6 -0
  184. package/src/lib/server/tool-retry.ts +2 -0
  185. package/src/lib/server/watch-jobs.test.ts +173 -0
  186. package/src/lib/server/watch-jobs.ts +532 -0
  187. package/src/lib/server/ws-hub.ts +5 -3
  188. package/src/lib/validation/schemas.test.ts +26 -0
  189. package/src/lib/validation/schemas.ts +7 -0
  190. package/src/lib/ws-client.ts +14 -12
  191. package/src/proxy.ts +5 -5
  192. package/src/stores/use-app-store.ts +0 -6
  193. package/src/stores/use-chat-store.ts +31 -2
  194. package/src/types/index.ts +287 -44
  195. package/src/components/chat/new-chat-sheet.tsx +0 -253
  196. package/src/lib/server/main-session.ts +0 -17
  197. package/src/lib/server/session-run-manager.test.ts +0 -26
@@ -22,7 +22,13 @@ import {
22
22
  import { resolveScheduleName } from '@/lib/schedule-name'
23
23
  import { findDuplicateSchedule, type ScheduleLike } from '@/lib/schedule-dedupe'
24
24
  import { computeTaskFingerprint, findDuplicateTask } from '@/lib/task-dedupe'
25
- import { resolveTaskAgentFromDescription } from '@/lib/server/task-mention'
25
+ import {
26
+ hasManagedAgentAssignmentInput,
27
+ isDelegationTaskPayload,
28
+ resolveDelegatorAgentId,
29
+ resolveManagedAgentAssignment,
30
+ validateManagedAgentAssignment,
31
+ } from '@/lib/server/agent-assignment'
26
32
  import { normalizeTaskQualityGate } from '@/lib/server/task-quality-gate'
27
33
  import type { ToolBuildContext } from './context'
28
34
  import { safePath, findBinaryOnPath } from './context'
@@ -137,7 +143,8 @@ const RESOURCE_DEFAULTS: Record<string, (parsed: any) => any> = {
137
143
  soul: p.soul || '',
138
144
  provider: p.provider || 'claude-cli',
139
145
  model: p.model || '',
140
- isOrchestrator: p.isOrchestrator || false,
146
+ platformAssignScope: p.platformAssignScope === 'all' ? 'all' : 'self',
147
+ isOrchestrator: p.platformAssignScope === 'all',
141
148
  tools: p.tools || [],
142
149
  skills: p.skills || [],
143
150
  skillIds: p.skillIds || [],
@@ -256,12 +263,12 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
256
263
  let description = `Manage SwarmClaw ${res.label}. ${res.readOnly ? 'List and get only.' : 'List, get, create, update, or delete.'} Returns JSON.`
257
264
  if (toolKey === 'manage_tasks') {
258
265
  if (assignScope === 'self') {
259
- description += `\n\nDo NOT create tasks for yourself just do the work directly. Tasks are for delegating work to other agents or for user-created work items. You can only list, get, update status, or complete tasks assigned to you ("${ctx?.agentId || 'unknown'}"). Valid manual statuses: backlog, queued, completed, failed, archived. "running" is runtime-only and set automatically when execution starts.`
266
+ description += `\n\nYou may create tasks for yourself ("${ctx?.agentId || 'unknown'}") or leave them unassigned to track multi-step work. You cannot assign tasks to other agents unless a user enables "Assign to Other Agents" in your agent settings. Valid manual statuses: backlog, queued, completed, failed, archived. "running" is runtime-only and set automatically when execution starts.`
260
267
  } else {
261
- description += `\n\nDo NOT create tasks for yourself just do the work directly. Only create tasks to delegate work to OTHER agents. Your agent ID is "${ctx?.agentId || 'unknown'}". Valid manual statuses: backlog, queued, completed, failed, archived. "running" is runtime-only and set automatically when execution starts.` + agentSummary
268
+ description += `\n\nYou may create tasks for yourself, leave them unassigned, or delegate them to other agents. Your agent ID is "${ctx?.agentId || 'unknown'}". When delegating, set a target agent using "agentId", "assignee", "agent", "assignedAgentId", or "assigned_agent_id". Use the target agent's exact ID when possible. Valid manual statuses: backlog, queued, completed, failed, archived. "running" is runtime-only and set automatically when execution starts.` + agentSummary
262
269
  }
263
270
  } else if (toolKey === 'manage_agents') {
264
- description += `\n\nAgents may self-edit their own soul. To update your soul, use action="update", id="${ctx?.agentId || 'your-agent-id'}", and include data with the "soul" field.`
271
+ description += `\n\nAgents may self-edit their own soul. To update your soul, use action="update", id="${ctx?.agentId || 'your-agent-id'}", and include data with the "soul" field. Set "platformAssignScope":"all" to let an agent delegate work across the fleet; use "self" for solo execution.`
265
272
  } else if (toolKey === 'manage_schedules') {
266
273
  if (assignScope === 'self') {
267
274
  description += `\n\nSet "agentId" to assign a schedule to yourself ("${ctx?.agentId || 'unknown'}") or leave it null. You can only assign schedules to yourself. Schedule types: interval (set intervalMs), cron (set cron), once (set runAt). Set taskPrompt for what the agent should do. Before create, call list/get to avoid duplicate schedules. If an equivalent active/paused schedule already exists, create returns that existing schedule (deduplicated=true).`
@@ -337,13 +344,30 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
337
344
  if (parsed && typeof parsed === 'object' && 'id' in parsed) {
338
345
  delete (parsed as Record<string, unknown>).id
339
346
  }
340
- // Enforce assignment scope for tasks and schedules
341
- if (assignScope === 'self' && (toolKey === 'manage_tasks' || toolKey === 'manage_schedules')) {
342
- if (parsed.agentId && parsed.agentId !== ctx?.agentId) {
343
- return `Error: You can only assign ${res.label} to yourself ("${ctx?.agentId}"). To assign to other agents, ask a user to enable "Assign to Other Agents" in your agent settings.`
344
- }
345
- }
346
347
  const now = Date.now()
348
+ if (toolKey === 'manage_tasks' || toolKey === 'manage_schedules') {
349
+ const agents = loadAgents()
350
+ const resolution = resolveManagedAgentAssignment(
351
+ parsed as Record<string, unknown>,
352
+ agents,
353
+ toolKey === 'manage_tasks' ? (parsed.agentId || ctx?.agentId || null) : null,
354
+ { allowDescription: toolKey === 'manage_tasks' },
355
+ )
356
+ const assignmentError = validateManagedAgentAssignment({
357
+ resourceLabel: res.label,
358
+ agents,
359
+ assignScope,
360
+ currentAgentId: ctx?.agentId || null,
361
+ targetAgentId: resolution.agentId,
362
+ unresolvedReference: resolution.unresolvedReference,
363
+ isDelegation: toolKey === 'manage_tasks' ? isDelegationTaskPayload(parsed as Record<string, unknown>) : false,
364
+ delegatorAgentId: toolKey === 'manage_tasks'
365
+ ? resolveDelegatorAgentId(parsed as Record<string, unknown>, agents, ctx?.agentId || null)
366
+ : null,
367
+ })
368
+ if (assignmentError) return assignmentError
369
+ parsed.agentId = resolution.agentId
370
+ }
347
371
  if (toolKey === 'manage_schedules') {
348
372
  const duplicate = findDuplicateSchedule(all as Record<string, ScheduleLike>, {
349
373
  agentId: parsed.agentId || null,
@@ -387,23 +411,6 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
387
411
  })
388
412
  }
389
413
  }
390
- // @mention agent resolution for tasks
391
- if (toolKey === 'manage_tasks' && parsed.description) {
392
- const agents = loadAgents()
393
- parsed.agentId = resolveTaskAgentFromDescription(
394
- parsed.description,
395
- parsed.agentId || ctx?.agentId || '',
396
- agents,
397
- )
398
- }
399
- // Agents cannot create tasks for themselves — just do the work directly.
400
- // Tasks are for delegating to other agents or user-created work items.
401
- if (toolKey === 'manage_tasks' && ctx?.agentId) {
402
- const resolvedAgentId = parsed.agentId || ctx.agentId
403
- if (resolvedAgentId === ctx.agentId) {
404
- return 'Error: You cannot create tasks for yourself — just do the work directly. Tasks are for delegating work to other agents. If you need to track progress, use memory instead.'
405
- }
406
- }
407
414
  if (toolKey === 'manage_tasks') {
408
415
  parsed.title = deriveTaskTitle(parsed)
409
416
  if (!parsed.title || /^untitled task$/i.test(parsed.title)) {
@@ -507,10 +514,41 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
507
514
  ? normalizeTaskQualityGate(parsed.qualityGate, settings)
508
515
  : null
509
516
  }
510
- // Enforce assignment scope for tasks and schedules
511
- if (assignScope === 'self' && (toolKey === 'manage_tasks' || toolKey === 'manage_schedules')) {
512
- if (parsed.agentId && parsed.agentId !== ctx?.agentId) {
513
- return `Error: You can only assign ${res.label} to yourself ("${ctx?.agentId}"). To assign to other agents, ask a user to enable "Assign to Other Agents" in your agent settings.`
517
+ if (toolKey === 'manage_tasks' || toolKey === 'manage_schedules') {
518
+ const agents = loadAgents()
519
+ const requestedClear = Object.prototype.hasOwnProperty.call(parsed, 'agentId') && parsed.agentId == null
520
+ const shouldResolveAssignment = requestedClear
521
+ || hasManagedAgentAssignmentInput(parsed as Record<string, unknown>)
522
+ if (shouldResolveAssignment) {
523
+ const resolution = resolveManagedAgentAssignment(
524
+ parsed as Record<string, unknown>,
525
+ agents,
526
+ null,
527
+ { allowDescription: false },
528
+ )
529
+ const assignmentError = validateManagedAgentAssignment({
530
+ resourceLabel: res.label,
531
+ agents,
532
+ assignScope,
533
+ currentAgentId: ctx?.agentId || null,
534
+ targetAgentId: requestedClear ? null : resolution.agentId,
535
+ unresolvedReference: requestedClear ? null : resolution.unresolvedReference,
536
+ isDelegation: toolKey === 'manage_tasks'
537
+ ? isDelegationTaskPayload({
538
+ ...all[id],
539
+ ...parsed,
540
+ agentId: requestedClear ? null : resolution.agentId,
541
+ } as Record<string, unknown>)
542
+ : false,
543
+ delegatorAgentId: toolKey === 'manage_tasks'
544
+ ? resolveDelegatorAgentId({
545
+ ...all[id],
546
+ ...parsed,
547
+ } as Record<string, unknown>, agents, ctx?.agentId || null)
548
+ : null,
549
+ })
550
+ if (assignmentError) return assignmentError
551
+ if (!requestedClear) parsed.agentId = resolution.agentId
514
552
  }
515
553
  }
516
554
  all[id] = { ...all[id], ...parsed, updatedAt: Date.now() }
@@ -0,0 +1,219 @@
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 writeExecutable(dir: string, name: string, source: string) {
11
+ const filePath = path.join(dir, name)
12
+ fs.writeFileSync(filePath, source, { mode: 0o755 })
13
+ return filePath
14
+ }
15
+
16
+ function runWithFakeDelegates(script: string) {
17
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-delegate-fallback-'))
18
+ try {
19
+ writeExecutable(tempDir, 'claude', `#!/bin/sh
20
+ if [ "$1" = "auth" ] && [ "$2" = "status" ]; then
21
+ echo '{"loggedIn":false}'
22
+ exit 1
23
+ fi
24
+ echo "unexpected claude invocation" >&2
25
+ exit 2
26
+ `)
27
+
28
+ writeExecutable(tempDir, 'codex', `#!/bin/sh
29
+ if [ "$1" = "login" ] && [ "$2" = "status" ]; then
30
+ echo 'logged in'
31
+ exit 0
32
+ fi
33
+ if [ "$1" = "exec" ]; then
34
+ cat >/dev/null
35
+ printf '%s\\n' '{"type":"item.completed","item":{"type":"agent_message","text":"codex fallback ok"}}'
36
+ exit 0
37
+ fi
38
+ echo "unexpected codex invocation" >&2
39
+ exit 2
40
+ `)
41
+
42
+ const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
43
+ cwd: repoRoot,
44
+ env: {
45
+ ...process.env,
46
+ PATH: `${tempDir}:${process.env.PATH || ''}`,
47
+ },
48
+ encoding: 'utf-8',
49
+ })
50
+ assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
51
+ const lines = (result.stdout || '')
52
+ .trim()
53
+ .split('\n')
54
+ .map((line) => line.trim())
55
+ .filter(Boolean)
56
+ const jsonLine = [...lines].reverse().find((line) => line.startsWith('{') || line.startsWith('['))
57
+ return JSON.parse(jsonLine || '{}')
58
+ } finally {
59
+ fs.rmSync(tempDir, { recursive: true, force: true })
60
+ }
61
+ }
62
+
63
+ describe('delegate fallback', () => {
64
+ it('falls back to another backend when Claude Code is unavailable', () => {
65
+ const output = runWithFakeDelegates(`
66
+ const mod = await import('./src/lib/server/session-tools/delegate.ts')
67
+ const { buildDelegateTools } = mod.default || mod['module.exports'] || mod
68
+
69
+ const tools = buildDelegateTools({
70
+ cwd: process.cwd(),
71
+ ctx: { sessionId: 'session-test', agentId: 'agent-test', platformAssignScope: 'self' },
72
+ hasPlugin: (name) => name === 'delegate',
73
+ hasTool: (name) => name === 'delegate',
74
+ cleanupFns: [],
75
+ commandTimeoutMs: 5000,
76
+ claudeTimeoutMs: 5000,
77
+ cliProcessTimeoutMs: 5000,
78
+ persistDelegateResumeId: () => {},
79
+ readStoredDelegateResumeId: () => null,
80
+ resolveCurrentSession: () => null,
81
+ activePlugins: ['delegate'],
82
+ })
83
+
84
+ const delegateTool = tools.find((tool) => tool.name === 'delegate')
85
+ const raw = await delegateTool.invoke({ task: 'write a helper', backend: 'claude' })
86
+ console.log(raw)
87
+ `)
88
+
89
+ assert.equal(output.backend, 'codex')
90
+ assert.equal(output.status, 'completed')
91
+ assert.match(String(output.response || ''), /codex fallback ok/i)
92
+ })
93
+
94
+ it('accepts wrapped function-call payloads with tool_name aliases', () => {
95
+ const output = runWithFakeDelegates(`
96
+ const mod = await import('./src/lib/server/session-tools/delegate.ts')
97
+ const { buildDelegateTools } = mod.default || mod['module.exports'] || mod
98
+
99
+ const tools = buildDelegateTools({
100
+ cwd: process.cwd(),
101
+ ctx: { sessionId: 'session-test', agentId: 'agent-test', platformAssignScope: 'self' },
102
+ hasPlugin: (name) => name === 'delegate',
103
+ hasTool: (name) => name === 'delegate',
104
+ cleanupFns: [],
105
+ commandTimeoutMs: 5000,
106
+ claudeTimeoutMs: 5000,
107
+ cliProcessTimeoutMs: 5000,
108
+ persistDelegateResumeId: () => {},
109
+ readStoredDelegateResumeId: () => null,
110
+ resolveCurrentSession: () => null,
111
+ activePlugins: ['delegate'],
112
+ })
113
+
114
+ const delegateTool = tools.find((tool) => tool.name === 'delegate')
115
+ const raw = await delegateTool.invoke({
116
+ input: JSON.stringify({
117
+ function: 'delegate',
118
+ parameters: {
119
+ tool_name: 'Claude Code',
120
+ parameters: {
121
+ task: 'Create a proof file',
122
+ },
123
+ },
124
+ }),
125
+ })
126
+ console.log(raw)
127
+ `)
128
+
129
+ assert.equal(output.backend, 'codex')
130
+ assert.equal(output.status, 'completed')
131
+ assert.match(String(output.response || ''), /codex fallback ok/i)
132
+ })
133
+
134
+ it('synthesizes a delegated task from write-style payloads', () => {
135
+ const output = runWithFakeDelegates(`
136
+ const mod = await import('./src/lib/server/session-tools/delegate.ts')
137
+ const { buildDelegateTools } = mod.default || mod['module.exports'] || mod
138
+
139
+ const tools = buildDelegateTools({
140
+ cwd: process.cwd(),
141
+ ctx: { sessionId: 'session-test', agentId: 'agent-test', platformAssignScope: 'self' },
142
+ hasPlugin: (name) => name === 'delegate',
143
+ hasTool: (name) => name === 'delegate',
144
+ cleanupFns: [],
145
+ commandTimeoutMs: 5000,
146
+ claudeTimeoutMs: 5000,
147
+ cliProcessTimeoutMs: 5000,
148
+ persistDelegateResumeId: () => {},
149
+ readStoredDelegateResumeId: () => null,
150
+ resolveCurrentSession: () => null,
151
+ activePlugins: ['delegate'],
152
+ })
153
+
154
+ const delegateTool = tools.find((tool) => tool.name === 'delegate')
155
+ const raw = await delegateTool.invoke({
156
+ input: JSON.stringify({
157
+ action: 'write',
158
+ target: 'delegate-proof.md',
159
+ content: 'Proof content',
160
+ }),
161
+ })
162
+ console.log(raw)
163
+ `)
164
+
165
+ assert.equal(output.backend, 'codex')
166
+ assert.equal(output.status, 'completed')
167
+ assert.match(String(output.response || ''), /codex fallback ok/i)
168
+ })
169
+
170
+ it('synthesizes a delegated task for action=start payloads that only provide files', () => {
171
+ const output = runWithFakeDelegates(`
172
+ const mod = await import('./src/lib/server/session-tools/delegate.ts')
173
+ const { buildDelegateTools } = mod.default || mod['module.exports'] || mod
174
+
175
+ const tools = buildDelegateTools({
176
+ cwd: process.cwd(),
177
+ ctx: { sessionId: 'session-test', agentId: 'agent-test', platformAssignScope: 'self' },
178
+ hasPlugin: (name) => name === 'delegate',
179
+ hasTool: (name) => name === 'delegate',
180
+ cleanupFns: [],
181
+ commandTimeoutMs: 5000,
182
+ claudeTimeoutMs: 5000,
183
+ cliProcessTimeoutMs: 5000,
184
+ persistDelegateResumeId: () => {},
185
+ readStoredDelegateResumeId: () => null,
186
+ resolveCurrentSession: () => null,
187
+ activePlugins: ['delegate'],
188
+ })
189
+
190
+ const delegateTool = tools.find((tool) => tool.name === 'delegate')
191
+ const raw = await delegateTool.invoke({
192
+ input: JSON.stringify({
193
+ action: 'start',
194
+ name: 'Create Weather Script',
195
+ files: [{
196
+ path: 'weather_update/weather_fetcher.py',
197
+ content: 'print("weather")',
198
+ }],
199
+ }),
200
+ })
201
+ console.log(raw)
202
+ `)
203
+
204
+ assert.equal(output.backend, 'codex')
205
+ assert.equal(output.status, 'completed')
206
+ assert.match(String(output.response || ''), /codex fallback ok/i)
207
+ })
208
+
209
+ it('ranks authenticated delegate backends ahead of unauthenticated ones', () => {
210
+ const output = runWithFakeDelegates(`
211
+ const mod = await import('./src/lib/server/provider-health.ts')
212
+ const { rankDelegatesByHealth } = mod.default || mod['module.exports'] || mod
213
+ const ranked = rankDelegatesByHealth(['delegate_to_claude_code', 'delegate_to_codex_cli'])
214
+ console.log(JSON.stringify(ranked))
215
+ `)
216
+
217
+ assert.deepEqual(output, ['delegate_to_codex_cli', 'delegate_to_claude_code'])
218
+ })
219
+ })