@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
@@ -1,20 +1,38 @@
1
1
  import { z } from 'zod'
2
2
  import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
- import { spawn, spawnSync } from 'child_process'
3
+ import { spawn, spawnSync, type ChildProcess } from 'child_process'
4
4
  import type { ToolBuildContext } from './context'
5
5
  import { truncate, findBinaryOnPath, MAX_OUTPUT } from './context'
6
6
  import type { Plugin, PluginHooks } from '@/types'
7
7
  import { getPluginManager } from '../plugins'
8
8
  import { normalizeToolInputArgs } from './normalize-tool-args'
9
+ import {
10
+ appendDelegationCheckpoint,
11
+ cancelDelegationJob,
12
+ completeDelegationJob,
13
+ createDelegationJob,
14
+ failDelegationJob,
15
+ getDelegationJob,
16
+ listDelegationJobs,
17
+ recoverStaleDelegationJobs,
18
+ registerDelegationRuntime,
19
+ startDelegationJob,
20
+ } from '../delegation-jobs'
21
+ import { markProviderFailure, markProviderSuccess } from '../provider-health'
9
22
 
10
23
  const MAX_DELEGATION_CHAIN_HOPS = 128
24
+ const DELEGATE_BACKEND_ORDER: DelegateBackend[] = ['claude', 'codex', 'opencode', 'gemini']
11
25
 
12
26
  interface DelegateContext {
27
+ id?: string
28
+ sessionId?: string | null
29
+ agentId?: string | null
30
+ jobId?: string | null
13
31
  cwd?: string
14
32
  claudeTimeoutMs?: number
15
33
  readStoredDelegateResumeId?: (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini') => string | null
16
- persistDelegateResumeId?: (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini', id: string) => void
17
- ctx?: { platformAssignScope?: string; agentId?: string | null }
34
+ persistDelegateResumeId?: (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini', id: string | null | undefined) => void
35
+ ctx?: { platformAssignScope?: string; agentId?: string | null; sessionId?: string | null }
18
36
  hasPlugin?: (name: string) => boolean
19
37
  /** @deprecated Use hasPlugin */
20
38
  hasTool?: (name: string) => boolean
@@ -22,6 +40,11 @@ interface DelegateContext {
22
40
 
23
41
  type DelegateBackend = 'claude' | 'codex' | 'opencode' | 'gemini'
24
42
 
43
+ interface DelegateRuntimeState {
44
+ child?: ChildProcess | null
45
+ cancel?: () => void
46
+ }
47
+
25
48
  function asTaskRecord(value: unknown): Record<string, unknown> | null {
26
49
  return value && typeof value === 'object' ? value as Record<string, unknown> : null
27
50
  }
@@ -63,11 +86,169 @@ function _computeDelegationDepth(
63
86
  return depth
64
87
  }
65
88
 
66
- /**
67
- * Core Delegate Execution Logic
68
- */
69
- async function executeDelegateAction(args: Record<string, unknown>, bctx: DelegateContext) {
70
- const normalized = normalizeToolInputArgs(args)
89
+ function sleep(ms: number) {
90
+ return new Promise((resolve) => setTimeout(resolve, ms))
91
+ }
92
+
93
+ function buildDelegateContextFromSessionish(session: unknown): DelegateContext {
94
+ const record = session && typeof session === 'object' ? session as Record<string, unknown> : {}
95
+ const sessionId = typeof record.id === 'string'
96
+ ? record.id
97
+ : typeof record.sessionId === 'string'
98
+ ? record.sessionId
99
+ : null
100
+ const agentId = typeof record.agentId === 'string' ? record.agentId : null
101
+ const platformAssignScope = typeof record.platformAssignScope === 'string' ? record.platformAssignScope : undefined
102
+ const storedResumeIds = record.delegateResumeIds && typeof record.delegateResumeIds === 'object'
103
+ ? record.delegateResumeIds as Record<string, unknown>
104
+ : null
105
+
106
+ return {
107
+ cwd: typeof record.cwd === 'string' ? record.cwd : process.cwd(),
108
+ claudeTimeoutMs: typeof record.claudeTimeoutMs === 'number' ? record.claudeTimeoutMs : undefined,
109
+ readStoredDelegateResumeId: typeof record.readStoredDelegateResumeId === 'function'
110
+ ? record.readStoredDelegateResumeId as DelegateContext['readStoredDelegateResumeId']
111
+ : (key) => {
112
+ const raw = storedResumeIds?.[key]
113
+ return typeof raw === 'string' && raw.trim() ? raw.trim() : null
114
+ },
115
+ persistDelegateResumeId: typeof record.persistDelegateResumeId === 'function'
116
+ ? record.persistDelegateResumeId as DelegateContext['persistDelegateResumeId']
117
+ : undefined,
118
+ id: typeof record.id === 'string' ? record.id : undefined,
119
+ sessionId,
120
+ agentId,
121
+ ctx: {
122
+ sessionId,
123
+ agentId,
124
+ platformAssignScope,
125
+ },
126
+ }
127
+ }
128
+
129
+ function buildDelegateResumePatch(bctx: DelegateContext) {
130
+ const resumeIds = {
131
+ claudeCode: bctx.readStoredDelegateResumeId?.('claudeCode') || null,
132
+ codex: bctx.readStoredDelegateResumeId?.('codex') || null,
133
+ opencode: bctx.readStoredDelegateResumeId?.('opencode') || null,
134
+ gemini: bctx.readStoredDelegateResumeId?.('gemini') || null,
135
+ }
136
+ const resumeId = resumeIds.claudeCode || resumeIds.codex || resumeIds.opencode || resumeIds.gemini || null
137
+ return { resumeIds, resumeId }
138
+ }
139
+
140
+ function coerceDelegateBackend(value: unknown): DelegateBackend | null {
141
+ const normalized = String(value || '').trim().toLowerCase()
142
+ if (!normalized) return null
143
+ if (['claude', 'claude code', 'claude-code', 'claude_code'].includes(normalized)) return 'claude'
144
+ if (['codex', 'codex cli', 'codex-cli', 'codex_cli'].includes(normalized)) return 'codex'
145
+ if (['opencode', 'open code', 'open-code', 'open_code'].includes(normalized)) return 'opencode'
146
+ if (['gemini', 'gemini cli', 'gemini-cli', 'gemini_cli'].includes(normalized)) return 'gemini'
147
+ return null
148
+ }
149
+
150
+ function buildDelegateTaskFromPayload(normalized: Record<string, unknown>): string | null {
151
+ const action = String(normalized.action || '').trim().toLowerCase()
152
+ const target = [
153
+ normalized.target,
154
+ normalized.path,
155
+ normalized.filePath,
156
+ normalized.filename,
157
+ normalized.name,
158
+ ].find((value) => typeof value === 'string' && value.trim()) as string | undefined
159
+ const content = typeof normalized.content === 'string' ? normalized.content.trim() : ''
160
+ const taskName = typeof normalized.name === 'string' ? normalized.name.trim() : ''
161
+ const files = Array.isArray(normalized.files) ? normalized.files : []
162
+ const fileInstructions = files
163
+ .filter((entry): entry is Record<string, unknown> => !!entry && typeof entry === 'object' && !Array.isArray(entry))
164
+ .map((entry) => {
165
+ const filePath = typeof entry.path === 'string'
166
+ ? entry.path.trim()
167
+ : typeof entry.filePath === 'string'
168
+ ? entry.filePath.trim()
169
+ : typeof entry.filename === 'string'
170
+ ? entry.filename.trim()
171
+ : ''
172
+ const fileContent = typeof entry.content === 'string' ? entry.content.trim() : ''
173
+ if (!filePath && !fileContent) return ''
174
+ if (filePath && fileContent) {
175
+ return `Create or update "${filePath}" with this content:\n\n${fileContent}`
176
+ }
177
+ if (filePath) return `Create or update "${filePath}".`
178
+ return `Create or update a file with this content:\n\n${fileContent}`
179
+ })
180
+ .filter(Boolean)
181
+
182
+ if (['write', 'create', 'create_file', 'create-file', 'createfile'].includes(action)) {
183
+ if (target && content) return `Create or overwrite the file "${target}" with this content:\n\n${content}`
184
+ if (target) return `Create the file "${target}".`
185
+ }
186
+ if (['edit', 'update', 'modify'].includes(action)) {
187
+ if (target && content) return `Update the file "${target}" with this content:\n\n${content}`
188
+ if (target) return `Update the file "${target}".`
189
+ }
190
+ if (target && content) return `Perform the "${action || 'requested'}" task against "${target}" using this content:\n\n${content}`
191
+ if (target) return `Perform the "${action || 'requested'}" task against "${target}".`
192
+ if (fileInstructions.length > 0) {
193
+ const intro = taskName || 'Perform the delegated file task.'
194
+ return `${intro}\n\n${fileInstructions.join('\n\n')}`
195
+ }
196
+ if (content) return `Perform the delegated task with this content:\n\n${content}`
197
+ if (taskName) return taskName
198
+ return null
199
+ }
200
+
201
+ function normalizeDelegateArgs(rawArgs: Record<string, unknown>): Record<string, unknown> {
202
+ const normalized = normalizeToolInputArgs(rawArgs)
203
+ const backend = coerceDelegateBackend(
204
+ normalized.backend
205
+ ?? normalized.tool_name
206
+ ?? normalized.toolName
207
+ ?? normalized.delegate
208
+ ?? normalized.provider,
209
+ )
210
+ if (backend && !normalized.backend) normalized.backend = backend
211
+ if (typeof normalized.task !== 'string' && typeof normalized.prompt === 'string') normalized.task = normalized.prompt
212
+ const action = String(normalized.action || '').trim().toLowerCase()
213
+ const isLifecycleAction = ['status', 'list', 'wait', 'cancel'].includes(action)
214
+ if (!isLifecycleAction) {
215
+ if (typeof normalized.task !== 'string' || !normalized.task.trim()) {
216
+ const synthesized = buildDelegateTaskFromPayload(normalized)
217
+ if (synthesized) normalized.task = synthesized
218
+ }
219
+ normalized.action = 'start'
220
+ }
221
+ return normalized
222
+ }
223
+
224
+ function resolveDelegateSessionId(bctx: DelegateContext): string | null {
225
+ const nested = typeof bctx.ctx?.sessionId === 'string' ? bctx.ctx.sessionId.trim() : ''
226
+ if (nested) return nested
227
+ const direct = typeof bctx.sessionId === 'string' ? bctx.sessionId.trim() : ''
228
+ if (direct) return direct
229
+ const legacy = typeof bctx.id === 'string' ? bctx.id.trim() : ''
230
+ return legacy || null
231
+ }
232
+
233
+ function bindDelegateRuntime(runtime: DelegateRuntimeState | undefined, child: ChildProcess) {
234
+ if (!runtime) return
235
+ runtime.child = child
236
+ runtime.cancel = () => {
237
+ try {
238
+ child.kill('SIGTERM')
239
+ } catch {
240
+ // best-effort cancel
241
+ }
242
+ }
243
+ const clear = () => {
244
+ if (runtime.child === child) runtime.child = null
245
+ }
246
+ child.once('close', clear)
247
+ child.once('error', clear)
248
+ }
249
+
250
+ async function runDelegateBackend(args: Record<string, unknown>, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<string> {
251
+ const normalized = normalizeDelegateArgs(args)
71
252
  const task = normalized.task as string
72
253
  const backend = ((normalized.backend as string) || 'claude') as DelegateBackend
73
254
  const resume = normalized.resume as boolean
@@ -81,13 +262,209 @@ async function executeDelegateAction(args: Record<string, unknown>, bctx: Delega
81
262
  const binary = backends[backend as keyof typeof backends]
82
263
  if (!binary) return `Error: Backend "${backend}" unavailable.`
83
264
 
84
- if (backend === 'claude') return runClaudeDelegate(binary, task, resume, resumeId, bctx)
85
- if (backend === 'codex') return runCodexDelegate(binary, task, resume, resumeId, bctx)
86
- if (backend === 'opencode') return runOpenCodeDelegate(binary, task, resume, resumeId, bctx)
87
- if (backend === 'gemini') return runGeminiDelegate(binary, task, resume, resumeId, bctx)
265
+ if (backend === 'claude') return runClaudeDelegate(binary, task, resume, resumeId, bctx, runtime)
266
+ if (backend === 'codex') return runCodexDelegate(binary, task, resume, resumeId, bctx, runtime)
267
+ if (backend === 'opencode') return runOpenCodeDelegate(binary, task, resume, resumeId, bctx, runtime)
268
+ if (backend === 'gemini') return runGeminiDelegate(binary, task, resume, resumeId, bctx, runtime)
88
269
  return `Error: Unsupported backend "${backend}".`
89
270
  }
90
271
 
272
+ function providerIdForBackend(backend: DelegateBackend): string {
273
+ if (backend === 'claude') return 'claude-cli'
274
+ if (backend === 'codex') return 'codex-cli'
275
+ if (backend === 'opencode') return 'opencode-cli'
276
+ return 'gemini-cli'
277
+ }
278
+
279
+ function fallbackOrderForBackend(requested: DelegateBackend): DelegateBackend[] {
280
+ return [requested, ...DELEGATE_BACKEND_ORDER.filter((backend) => backend !== requested)]
281
+ }
282
+
283
+ function isRecoverableDelegateFailure(result: string): boolean {
284
+ const normalized = String(result || '').trim().toLowerCase()
285
+ if (!normalized.startsWith('error:')) return false
286
+ return [
287
+ 'not authenticated',
288
+ 'backend "',
289
+ 'unavailable',
290
+ 'enoent',
291
+ 'not found',
292
+ 'command not found',
293
+ 'spawn ',
294
+ 'eacces',
295
+ 'permission denied',
296
+ ].some((needle) => normalized.includes(needle))
297
+ }
298
+
299
+ function summarizeDelegateAttempts(
300
+ requested: DelegateBackend,
301
+ attempts: Array<{ backend: DelegateBackend; result: string }>,
302
+ ): string {
303
+ const summary = attempts
304
+ .map(({ backend, result }) => `${backend}: ${result.replace(/^Error:\s*/i, '').trim() || result.trim()}`)
305
+ .join(' | ')
306
+ return `Error: Delegate backend "${requested}" could not complete the task. ${summary}. Continue with another available tool instead of stopping.`
307
+ }
308
+
309
+ async function runDelegateBackendWithFallback(
310
+ args: Record<string, unknown>,
311
+ bctx: DelegateContext,
312
+ runtime?: DelegateRuntimeState,
313
+ opts?: { onAttempt?: (backend: DelegateBackend, attemptIndex: number) => void; onFallback?: (from: DelegateBackend, to: DelegateBackend, reason: string) => void },
314
+ ): Promise<{ backend: DelegateBackend; result: string; attempts: Array<{ backend: DelegateBackend; result: string }> }> {
315
+ const normalized = normalizeDelegateArgs(args)
316
+ const requested = ((normalized.backend as string) || 'claude') as DelegateBackend
317
+ const orderedBackends = fallbackOrderForBackend(requested)
318
+ const attempts: Array<{ backend: DelegateBackend; result: string }> = []
319
+
320
+ for (const [index, backend] of orderedBackends.entries()) {
321
+ opts?.onAttempt?.(backend, index)
322
+ const result = await runDelegateBackend({ ...normalized, backend }, bctx, runtime)
323
+ attempts.push({ backend, result })
324
+ if (/^Error:/i.test(result.trim())) {
325
+ markProviderFailure(providerIdForBackend(backend), result)
326
+ } else {
327
+ markProviderSuccess(providerIdForBackend(backend))
328
+ return { backend, result, attempts }
329
+ }
330
+
331
+ const nextBackend = orderedBackends[index + 1]
332
+ if (nextBackend && isRecoverableDelegateFailure(result)) {
333
+ opts?.onFallback?.(backend, nextBackend, result)
334
+ continue
335
+ }
336
+ return {
337
+ backend,
338
+ result: attempts.length > 1 ? summarizeDelegateAttempts(requested, attempts) : result,
339
+ attempts,
340
+ }
341
+ }
342
+
343
+ return {
344
+ backend: requested,
345
+ result: summarizeDelegateAttempts(requested, attempts),
346
+ attempts,
347
+ }
348
+ }
349
+
350
+ async function waitForDelegateJob(jobId: string, timeoutSec = 30): Promise<string> {
351
+ const timeoutAt = Date.now() + Math.max(1, timeoutSec) * 1000
352
+ while (Date.now() < timeoutAt) {
353
+ const job = getDelegationJob(jobId)
354
+ if (!job) return `Error: delegation job "${jobId}" not found.`
355
+ if (job.status === 'completed' || job.status === 'failed' || job.status === 'cancelled') {
356
+ return JSON.stringify(job)
357
+ }
358
+ await sleep(1000)
359
+ }
360
+ const latest = getDelegationJob(jobId)
361
+ return latest ? JSON.stringify(latest) : `Error: delegation job "${jobId}" not found.`
362
+ }
363
+
364
+ /**
365
+ * Core Delegate Execution Logic
366
+ */
367
+ async function executeDelegateAction(args: Record<string, unknown>, bctx: DelegateContext) {
368
+ const normalized = normalizeDelegateArgs(args)
369
+ const action = String(normalized.action || '').trim().toLowerCase()
370
+ const task = normalized.task as string
371
+ const requestedBackend = ((normalized.backend as string) || 'claude') as DelegateBackend
372
+ const jobId = typeof normalized.jobId === 'string' ? normalized.jobId.trim() : ''
373
+ const waitForCompletion = normalized.waitForCompletion !== false && normalized.background !== true
374
+ const parentSessionId = resolveDelegateSessionId(bctx)
375
+
376
+ recoverStaleDelegationJobs()
377
+
378
+ if (action === 'status') {
379
+ if (!jobId) return 'Error: jobId is required.'
380
+ const job = getDelegationJob(jobId)
381
+ return job ? JSON.stringify(job) : `Error: delegation job "${jobId}" not found.`
382
+ }
383
+ if (action === 'list') {
384
+ const jobs = listDelegationJobs({ parentSessionId: parentSessionId || null })
385
+ .filter((job) => job.kind === 'delegate')
386
+ return JSON.stringify(jobs)
387
+ }
388
+ if (action === 'cancel') {
389
+ if (!jobId) return 'Error: jobId is required.'
390
+ const job = cancelDelegationJob(jobId)
391
+ return job ? JSON.stringify(job) : `Error: delegation job "${jobId}" not found.`
392
+ }
393
+ if (action === 'wait') {
394
+ if (!jobId) return 'Error: jobId is required.'
395
+ const timeoutSec = typeof normalized.timeoutSec === 'number' ? normalized.timeoutSec : 30
396
+ return waitForDelegateJob(jobId, timeoutSec)
397
+ }
398
+
399
+ if (!task) return 'Error: task is required.'
400
+
401
+ const job = createDelegationJob({
402
+ kind: 'delegate',
403
+ parentSessionId,
404
+ backend: requestedBackend,
405
+ task,
406
+ cwd: bctx.cwd || null,
407
+ })
408
+ appendDelegationCheckpoint(job.id, `Dispatching to ${requestedBackend}`, 'queued')
409
+ startDelegationJob(job.id, { backend: requestedBackend, cwd: bctx.cwd || null })
410
+ const runtimeHandle: DelegateRuntimeState = {}
411
+ registerDelegationRuntime(job.id, runtimeHandle)
412
+
413
+ const runner = runDelegateBackendWithFallback(args, bctx, runtimeHandle, {
414
+ onAttempt: (backend, index) => {
415
+ if (index === 0) return
416
+ appendDelegationCheckpoint(job.id, `Retrying delegate with ${backend}`, 'running')
417
+ startDelegationJob(job.id, { backend, cwd: bctx.cwd || null })
418
+ },
419
+ onFallback: (from, to, reason) => {
420
+ appendDelegationCheckpoint(
421
+ job.id,
422
+ `Delegate ${from} failed: ${reason.replace(/^Error:\s*/i, '').trim()}. Falling back to ${to}.`,
423
+ 'running',
424
+ )
425
+ },
426
+ })
427
+ .then(({ backend, result }) => {
428
+ const latest = getDelegationJob(job.id)
429
+ if (latest?.status === 'cancelled') return { backend, result }
430
+ const resumePatch = buildDelegateResumePatch(bctx)
431
+ if (/^Error:/i.test(result.trim())) {
432
+ appendDelegationCheckpoint(job.id, `Delegate failed on ${backend}`, 'failed')
433
+ failDelegationJob(job.id, result.replace(/^Error:\s*/i, '').trim() || result, { ...resumePatch, backend })
434
+ } else {
435
+ appendDelegationCheckpoint(job.id, `Delegate completed on ${backend}`, 'completed')
436
+ completeDelegationJob(job.id, result, { ...resumePatch, backend })
437
+ }
438
+ return { backend, result }
439
+ })
440
+ .catch((err: unknown) => {
441
+ const message = err instanceof Error ? err.message : String(err)
442
+ const latest = getDelegationJob(job.id)
443
+ if (latest?.status === 'cancelled') return { backend: requestedBackend, result: `Error: ${message}` }
444
+ appendDelegationCheckpoint(job.id, `Delegate crashed on ${requestedBackend}: ${message}`, 'failed')
445
+ failDelegationJob(job.id, message, { ...buildDelegateResumePatch(bctx), backend: requestedBackend })
446
+ return { backend: requestedBackend, result: `Error: ${message}` }
447
+ })
448
+
449
+ if (!waitForCompletion) {
450
+ void runner
451
+ return JSON.stringify({
452
+ jobId: job.id,
453
+ status: 'running',
454
+ backend: requestedBackend,
455
+ })
456
+ }
457
+
458
+ const { backend, result } = await runner
459
+ const latest = getDelegationJob(job.id)
460
+ return JSON.stringify({
461
+ jobId: job.id,
462
+ status: latest?.status || (/^Error:/i.test(result.trim()) ? 'failed' : 'completed'),
463
+ backend: latest?.backend || backend,
464
+ response: result,
465
+ })
466
+ }
467
+
91
468
  function stripEnvPrefixes(input: NodeJS.ProcessEnv, prefixes: string[]): NodeJS.ProcessEnv {
92
469
  const out: NodeJS.ProcessEnv = { ...input }
93
470
  for (const key of Object.keys(out)) {
@@ -120,7 +497,7 @@ function parseCodexOutputText(ev: Record<string, unknown>): string | null {
120
497
  return null
121
498
  }
122
499
 
123
- async function runCodexDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext): Promise<string> {
500
+ async function runCodexDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<string> {
124
501
  try {
125
502
  const env = stripEnvPrefixes({ ...process.env, TERM: 'dumb', NO_COLOR: '1' }, ['CODEX'])
126
503
  const authProbe = spawnSync(binary, ['login', 'status'], { cwd: bctx.cwd, env, encoding: 'utf-8', timeout: 8000 })
@@ -139,6 +516,7 @@ async function runCodexDelegate(binary: string, task: string, resume: boolean, r
139
516
  args.push('--json', '--full-auto', '--skip-git-repo-check', '-')
140
517
 
141
518
  const child = spawn(binary, args, { cwd: bctx.cwd, env, stdio: ['pipe', 'pipe', 'pipe'] })
519
+ bindDelegateRuntime(runtime, child)
142
520
  let stdoutBuf = ''
143
521
  let stderrBuf = ''
144
522
  let responseText = ''
@@ -201,7 +579,7 @@ async function runCodexDelegate(binary: string, task: string, resume: boolean, r
201
579
  }
202
580
  }
203
581
 
204
- async function runOpenCodeDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext): Promise<string> {
582
+ async function runOpenCodeDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<string> {
205
583
  try {
206
584
  const env = { ...process.env, TERM: 'dumb', NO_COLOR: '1' } as NodeJS.ProcessEnv
207
585
  const storedResumeId = bctx.readStoredDelegateResumeId?.('opencode')
@@ -212,6 +590,7 @@ async function runOpenCodeDelegate(binary: string, task: string, resume: boolean
212
590
  if (resumeIdToUse) args.push('--session', resumeIdToUse)
213
591
 
214
592
  const child = spawn(binary, args, { cwd: bctx.cwd, env, stdio: ['ignore', 'pipe', 'pipe'] })
593
+ bindDelegateRuntime(runtime, child)
215
594
  let stdoutBuf = ''
216
595
  let stderrBuf = ''
217
596
  let responseText = ''
@@ -277,7 +656,7 @@ async function runOpenCodeDelegate(binary: string, task: string, resume: boolean
277
656
  }
278
657
  }
279
658
 
280
- async function runGeminiDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext): Promise<string> {
659
+ async function runGeminiDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<string> {
281
660
  try {
282
661
  const env = { ...process.env, TERM: 'dumb', NO_COLOR: '1' } as NodeJS.ProcessEnv
283
662
  const storedResumeId = bctx.readStoredDelegateResumeId?.('gemini')
@@ -288,6 +667,7 @@ async function runGeminiDelegate(binary: string, task: string, resume: boolean,
288
667
  if (resumeIdToUse) args.push('--resume', resumeIdToUse)
289
668
 
290
669
  const child = spawn(binary, args, { cwd: bctx.cwd, env, stdio: ['ignore', 'pipe', 'pipe'] })
670
+ bindDelegateRuntime(runtime, child)
291
671
  let stdoutBuf = ''
292
672
  let stderrBuf = ''
293
673
  let responseText = ''
@@ -357,7 +737,7 @@ async function runGeminiDelegate(binary: string, task: string, resume: boolean,
357
737
  }
358
738
  }
359
739
 
360
- async function runClaudeDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext): Promise<string> {
740
+ async function runClaudeDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<string> {
361
741
  try {
362
742
  const env: NodeJS.ProcessEnv = stripEnvPrefixes({ ...process.env }, ['CLAUDE'])
363
743
  const authProbe = spawnSync(binary, ['auth', 'status'], { cwd: bctx.cwd, env, encoding: 'utf-8', timeout: 8000 })
@@ -370,6 +750,7 @@ async function runClaudeDelegate(binary: string, task: string, resume: boolean,
370
750
  const args = ['--print', '--output-format', 'stream-json', '--verbose', '--dangerously-skip-permissions']
371
751
  if (resumeIdToUse) args.push('--resume', resumeIdToUse)
372
752
  const child = spawn(binary, args, { cwd: bctx.cwd, env, stdio: ['pipe', 'pipe', 'pipe'] })
753
+ bindDelegateRuntime(runtime, child)
373
754
  let stderr = ''
374
755
  let assistantText = ''
375
756
  let discoveredId: string | null = null
@@ -426,18 +807,23 @@ const DelegatePlugin: Plugin = {
426
807
  tools: [
427
808
  {
428
809
  name: 'delegate',
429
- description: 'Delegate to a specialized backend (Claude, Codex, OpenCode, Gemini).',
810
+ description: 'Delegate to a specialized backend (Claude, Codex, OpenCode, Gemini). Supports background jobs with action=status|list|wait|cancel.',
430
811
  parameters: {
431
812
  type: 'object',
432
813
  properties: {
814
+ action: { type: 'string', enum: ['start', 'status', 'list', 'wait', 'cancel'] },
433
815
  task: { type: 'string' },
434
816
  backend: { type: 'string', enum: ['claude', 'codex', 'opencode', 'gemini'] },
435
817
  resume: { type: 'boolean' },
436
- resumeId: { type: 'string', description: 'Optional explicit session/thread ID to resume' }
818
+ resumeId: { type: 'string', description: 'Optional explicit session/thread ID to resume' },
819
+ jobId: { type: 'string' },
820
+ waitForCompletion: { type: 'boolean' },
821
+ background: { type: 'boolean' },
822
+ timeoutSec: { type: 'number' },
437
823
  },
438
- required: ['task']
824
+ required: []
439
825
  },
440
- execute: async (args, context) => executeDelegateAction(args, { ...context.session, cwd: context.session.cwd || process.cwd() })
826
+ execute: async (args, context) => executeDelegateAction(args, buildDelegateContextFromSessionish(context.session))
441
827
  }
442
828
  ]
443
829
  }
@@ -96,8 +96,8 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
96
96
  })
97
97
  }
98
98
  }
99
- const { requestApproval } = await import('../approvals')
100
- requestApproval({
99
+ const { requestApprovalMaybeAutoApprove } = await import('../approvals')
100
+ const approval = await requestApprovalMaybeAutoApprove({
101
101
  category: 'tool_access',
102
102
  title: `Enable Plugin: ${pluginId}`,
103
103
  description: reason || `Agent is requesting access to the "${pluginId}" plugin.`,
@@ -105,6 +105,15 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
105
105
  agentId: bctx?.ctx?.agentId,
106
106
  sessionId: bctx?.ctx?.sessionId,
107
107
  })
108
+ if (approval.status === 'approved') {
109
+ return JSON.stringify({
110
+ alreadyGranted: true,
111
+ pluginId,
112
+ toolId: pluginId,
113
+ autoApproved: true,
114
+ message: `Access to "${pluginId}" was auto-approved and granted. Proceed to use it directly.`,
115
+ })
116
+ }
108
117
  return JSON.stringify({
109
118
  type: 'plugin_request',
110
119
  pluginId,
@@ -118,8 +127,8 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
118
127
  return JSON.stringify({ error: 'url is required for install_request.' })
119
128
  }
120
129
  if (approved !== true) {
121
- const { requestApproval } = await import('../approvals')
122
- requestApproval({
130
+ const { requestApprovalMaybeAutoApprove } = await import('../approvals')
131
+ const approval = await requestApprovalMaybeAutoApprove({
123
132
  category: 'plugin_install',
124
133
  title: `Install Plugin${pluginId ? `: ${pluginId}` : ' from URL'}`,
125
134
  description: reason || `Agent wants to install a plugin${url ? ` from ${url}` : ''}.`,
@@ -127,6 +136,15 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
127
136
  agentId: bctx?.ctx?.agentId,
128
137
  sessionId: bctx?.ctx?.sessionId,
129
138
  })
139
+ if (approval.status === 'approved') {
140
+ return JSON.stringify({
141
+ type: 'plugin_install_request',
142
+ url,
143
+ pluginId,
144
+ autoApproved: true,
145
+ message: `Plugin install from ${url} was auto-approved and has been applied.`,
146
+ })
147
+ }
130
148
  return JSON.stringify({
131
149
  type: 'plugin_install_request',
132
150
  url,