@swarmclawai/swarmclaw 0.6.7 → 0.7.0

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 (203) hide show
  1. package/README.md +82 -39
  2. package/next.config.ts +31 -6
  3. package/package.json +3 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +1 -0
  5. package/src/app/api/agents/route.ts +19 -5
  6. package/src/app/api/approvals/route.ts +22 -0
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
  8. package/src/app/api/clawhub/install/route.ts +2 -2
  9. package/src/app/api/eval/run/route.ts +37 -0
  10. package/src/app/api/eval/scenarios/route.ts +24 -0
  11. package/src/app/api/eval/suite/route.ts +29 -0
  12. package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
  13. package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
  14. package/src/app/api/memory/graph/route.ts +46 -0
  15. package/src/app/api/memory/route.ts +36 -5
  16. package/src/app/api/notifications/route.ts +3 -0
  17. package/src/app/api/plugins/install/route.ts +57 -5
  18. package/src/app/api/plugins/marketplace/route.ts +73 -22
  19. package/src/app/api/plugins/route.ts +61 -1
  20. package/src/app/api/plugins/ui/route.ts +34 -0
  21. package/src/app/api/sessions/[id]/checkpoints/route.ts +31 -0
  22. package/src/app/api/sessions/[id]/restore/route.ts +36 -0
  23. package/src/app/api/settings/route.ts +62 -0
  24. package/src/app/api/setup/doctor/route.ts +22 -5
  25. package/src/app/api/souls/[id]/route.ts +65 -0
  26. package/src/app/api/souls/route.ts +70 -0
  27. package/src/app/api/tasks/[id]/approve/route.ts +4 -3
  28. package/src/app/api/tasks/[id]/route.ts +16 -3
  29. package/src/app/api/tasks/route.ts +10 -2
  30. package/src/app/api/usage/route.ts +9 -2
  31. package/src/app/globals.css +27 -0
  32. package/src/app/page.tsx +10 -5
  33. package/src/cli/index.js +37 -0
  34. package/src/components/activity/activity-feed.tsx +9 -2
  35. package/src/components/agents/agent-avatar.tsx +5 -1
  36. package/src/components/agents/agent-card.tsx +55 -9
  37. package/src/components/agents/agent-sheet.tsx +112 -34
  38. package/src/components/agents/inspector-panel.tsx +1 -1
  39. package/src/components/agents/soul-library-picker.tsx +84 -13
  40. package/src/components/auth/access-key-gate.tsx +63 -54
  41. package/src/components/auth/user-picker.tsx +37 -32
  42. package/src/components/chat/activity-moment.tsx +2 -0
  43. package/src/components/chat/chat-area.tsx +11 -0
  44. package/src/components/chat/chat-header.tsx +69 -25
  45. package/src/components/chat/chat-tool-toggles.tsx +2 -2
  46. package/src/components/chat/checkpoint-timeline.tsx +112 -0
  47. package/src/components/chat/code-block.tsx +3 -1
  48. package/src/components/chat/exec-approval-card.tsx +8 -1
  49. package/src/components/chat/message-bubble.tsx +164 -4
  50. package/src/components/chat/message-list.tsx +46 -4
  51. package/src/components/chat/session-approval-card.tsx +80 -0
  52. package/src/components/chat/session-debug-panel.tsx +106 -84
  53. package/src/components/chat/streaming-bubble.tsx +6 -5
  54. package/src/components/chat/task-approval-card.tsx +78 -0
  55. package/src/components/chat/thinking-indicator.tsx +48 -12
  56. package/src/components/chat/tool-call-bubble.tsx +3 -0
  57. package/src/components/chat/tool-request-banner.tsx +39 -20
  58. package/src/components/chatrooms/chatroom-list.tsx +11 -4
  59. package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
  60. package/src/components/connectors/connector-list.tsx +33 -11
  61. package/src/components/connectors/connector-sheet.tsx +37 -7
  62. package/src/components/home/home-view.tsx +54 -24
  63. package/src/components/input/chat-input.tsx +22 -1
  64. package/src/components/knowledge/knowledge-list.tsx +17 -18
  65. package/src/components/knowledge/knowledge-sheet.tsx +9 -5
  66. package/src/components/layout/app-layout.tsx +87 -19
  67. package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
  68. package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
  69. package/src/components/memory/memory-browser.tsx +73 -45
  70. package/src/components/memory/memory-graph-view.tsx +203 -0
  71. package/src/components/memory/memory-list.tsx +20 -13
  72. package/src/components/plugins/plugin-list.tsx +214 -60
  73. package/src/components/plugins/plugin-sheet.tsx +119 -24
  74. package/src/components/projects/project-list.tsx +17 -9
  75. package/src/components/providers/provider-list.tsx +21 -6
  76. package/src/components/providers/provider-sheet.tsx +42 -25
  77. package/src/components/runs/run-list.tsx +17 -13
  78. package/src/components/schedules/schedule-card.tsx +10 -3
  79. package/src/components/schedules/schedule-list.tsx +2 -2
  80. package/src/components/schedules/schedule-sheet.tsx +28 -9
  81. package/src/components/secrets/secret-sheet.tsx +7 -2
  82. package/src/components/secrets/secrets-list.tsx +18 -5
  83. package/src/components/sessions/new-session-sheet.tsx +183 -376
  84. package/src/components/sessions/session-card.tsx +10 -2
  85. package/src/components/settings/gateway-connection-panel.tsx +9 -8
  86. package/src/components/shared/command-palette.tsx +13 -5
  87. package/src/components/shared/empty-state.tsx +20 -8
  88. package/src/components/shared/hint-tip.tsx +31 -0
  89. package/src/components/shared/notification-center.tsx +134 -86
  90. package/src/components/shared/profile-sheet.tsx +4 -0
  91. package/src/components/shared/settings/plugin-manager.tsx +360 -135
  92. package/src/components/shared/settings/section-capability-policy.tsx +3 -3
  93. package/src/components/shared/settings/section-runtime-loop.tsx +149 -4
  94. package/src/components/skills/clawhub-browser.tsx +1 -0
  95. package/src/components/skills/skill-list.tsx +31 -12
  96. package/src/components/skills/skill-sheet.tsx +20 -7
  97. package/src/components/tasks/approvals-panel.tsx +224 -0
  98. package/src/components/tasks/task-board.tsx +20 -12
  99. package/src/components/tasks/task-card.tsx +21 -7
  100. package/src/components/tasks/task-column.tsx +4 -3
  101. package/src/components/tasks/task-list.tsx +1 -1
  102. package/src/components/tasks/task-sheet.tsx +130 -1
  103. package/src/components/ui/dialog.tsx +1 -0
  104. package/src/components/ui/sheet.tsx +1 -0
  105. package/src/components/usage/metrics-dashboard.tsx +72 -48
  106. package/src/components/wallets/wallet-panel.tsx +65 -41
  107. package/src/components/wallets/wallet-section.tsx +9 -3
  108. package/src/components/webhooks/webhook-list.tsx +21 -12
  109. package/src/components/webhooks/webhook-sheet.tsx +13 -3
  110. package/src/lib/approval-display.test.ts +45 -0
  111. package/src/lib/approval-display.ts +62 -0
  112. package/src/lib/clipboard.ts +38 -0
  113. package/src/lib/memory.ts +8 -0
  114. package/src/lib/providers/claude-cli.ts +5 -3
  115. package/src/lib/providers/index.ts +67 -21
  116. package/src/lib/runtime-loop.ts +3 -2
  117. package/src/lib/server/approvals.ts +150 -0
  118. package/src/lib/server/chat-execution.ts +319 -74
  119. package/src/lib/server/chatroom-helpers.ts +63 -5
  120. package/src/lib/server/chatroom-orchestration.ts +74 -0
  121. package/src/lib/server/clawhub-client.ts +82 -6
  122. package/src/lib/server/connectors/manager.ts +27 -1
  123. package/src/lib/server/context-manager.ts +132 -50
  124. package/src/lib/server/cost.test.ts +73 -0
  125. package/src/lib/server/cost.ts +165 -34
  126. package/src/lib/server/daemon-state.ts +112 -1
  127. package/src/lib/server/data-dir.ts +18 -1
  128. package/src/lib/server/eval/runner.ts +126 -0
  129. package/src/lib/server/eval/scenarios.ts +218 -0
  130. package/src/lib/server/eval/scorer.ts +96 -0
  131. package/src/lib/server/eval/store.ts +37 -0
  132. package/src/lib/server/eval/types.ts +48 -0
  133. package/src/lib/server/execution-log.ts +12 -8
  134. package/src/lib/server/guardian.ts +34 -0
  135. package/src/lib/server/heartbeat-service.ts +53 -1
  136. package/src/lib/server/integrity-monitor.ts +208 -0
  137. package/src/lib/server/langgraph-checkpoint.ts +10 -0
  138. package/src/lib/server/link-understanding.ts +55 -0
  139. package/src/lib/server/llm-response-cache.test.ts +102 -0
  140. package/src/lib/server/llm-response-cache.ts +227 -0
  141. package/src/lib/server/main-agent-loop.ts +115 -16
  142. package/src/lib/server/main-session.ts +6 -3
  143. package/src/lib/server/mcp-conformance.test.ts +18 -0
  144. package/src/lib/server/mcp-conformance.ts +233 -0
  145. package/src/lib/server/memory-db.ts +193 -19
  146. package/src/lib/server/memory-retrieval.test.ts +56 -0
  147. package/src/lib/server/mmr.ts +73 -0
  148. package/src/lib/server/orchestrator-lg.ts +7 -1
  149. package/src/lib/server/orchestrator.ts +4 -3
  150. package/src/lib/server/plugins.ts +662 -132
  151. package/src/lib/server/process-manager.ts +18 -0
  152. package/src/lib/server/query-expansion.ts +57 -0
  153. package/src/lib/server/queue.ts +280 -11
  154. package/src/lib/server/runtime-settings.ts +9 -0
  155. package/src/lib/server/session-run-manager.test.ts +23 -0
  156. package/src/lib/server/session-run-manager.ts +32 -2
  157. package/src/lib/server/session-tools/canvas.ts +85 -50
  158. package/src/lib/server/session-tools/chatroom.ts +130 -127
  159. package/src/lib/server/session-tools/connector.ts +233 -454
  160. package/src/lib/server/session-tools/context-mgmt.ts +87 -105
  161. package/src/lib/server/session-tools/crud.ts +84 -7
  162. package/src/lib/server/session-tools/delegate.ts +351 -752
  163. package/src/lib/server/session-tools/discovery.ts +198 -0
  164. package/src/lib/server/session-tools/edit_file.ts +82 -0
  165. package/src/lib/server/session-tools/file-send.test.ts +39 -0
  166. package/src/lib/server/session-tools/file.ts +257 -425
  167. package/src/lib/server/session-tools/git.ts +87 -47
  168. package/src/lib/server/session-tools/http.ts +95 -33
  169. package/src/lib/server/session-tools/index.ts +217 -138
  170. package/src/lib/server/session-tools/memory.ts +154 -239
  171. package/src/lib/server/session-tools/monitor.ts +126 -0
  172. package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
  173. package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
  174. package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
  175. package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
  176. package/src/lib/server/session-tools/platform.ts +86 -0
  177. package/src/lib/server/session-tools/plugin-creator.ts +239 -0
  178. package/src/lib/server/session-tools/sample-ui.ts +97 -0
  179. package/src/lib/server/session-tools/sandbox.ts +175 -148
  180. package/src/lib/server/session-tools/schedule.ts +78 -0
  181. package/src/lib/server/session-tools/session-info.ts +104 -410
  182. package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
  183. package/src/lib/server/session-tools/shell.ts +171 -143
  184. package/src/lib/server/session-tools/subagent.ts +77 -77
  185. package/src/lib/server/session-tools/wallet.ts +182 -106
  186. package/src/lib/server/session-tools/web.ts +181 -327
  187. package/src/lib/server/storage.ts +36 -0
  188. package/src/lib/server/stream-agent-chat.ts +348 -242
  189. package/src/lib/server/task-quality-gate.test.ts +44 -0
  190. package/src/lib/server/task-quality-gate.ts +67 -0
  191. package/src/lib/server/task-validation.test.ts +78 -0
  192. package/src/lib/server/task-validation.ts +67 -2
  193. package/src/lib/server/tool-aliases.ts +68 -0
  194. package/src/lib/server/tool-capability-policy.ts +24 -5
  195. package/src/lib/server/tool-retry.ts +62 -0
  196. package/src/lib/server/transcript-repair.ts +72 -0
  197. package/src/lib/setup-defaults.ts +1 -0
  198. package/src/lib/tasks.ts +7 -1
  199. package/src/lib/tool-definitions.ts +24 -23
  200. package/src/lib/validation/schemas.ts +13 -0
  201. package/src/lib/view-routes.ts +2 -23
  202. package/src/stores/use-app-store.ts +23 -1
  203. package/src/types/index.ts +155 -10
@@ -5,6 +5,7 @@ const MAX_LOG_CHARS = 200_000
5
5
  const DEFAULT_BACKGROUND_YIELD_MS = 10_000
6
6
  const DEFAULT_TIMEOUT_MS = 30 * 60_000
7
7
  const DEFAULT_TTL_MS = 30 * 60_000
8
+ const BACKGROUND_STARTUP_GRACE_MS = 500
8
9
 
9
10
  export type ProcessStatus = 'running' | 'exited' | 'killed' | 'failed' | 'timeout'
10
11
 
@@ -170,6 +171,23 @@ export async function startManagedProcess(opts: StartProcessOptions): Promise<St
170
171
  state.exitWaiters.set(id, exitPromise)
171
172
 
172
173
  if (opts.background) {
174
+ // Give background processes a brief grace window so immediate crashes
175
+ // (e.g., bind/permission errors) are surfaced instead of misreported as running.
176
+ const startupWaitMs = Math.min(
177
+ Math.max(100, BACKGROUND_STARTUP_GRACE_MS),
178
+ Math.max(200, timeoutMs),
179
+ )
180
+ await wait(startupWaitMs)
181
+ const rec = state.records.get(id)
182
+ if (rec && rec.status !== 'running') {
183
+ return {
184
+ status: 'completed',
185
+ processId: id,
186
+ output: rec.log,
187
+ exitCode: rec.exitCode,
188
+ signal: rec.signal,
189
+ }
190
+ }
173
191
  return {
174
192
  status: 'running',
175
193
  processId: id,
@@ -0,0 +1,57 @@
1
+ import { loadAgents, loadSettings, loadCredentials, decryptKey } from './storage'
2
+ import { getProvider } from '../providers'
3
+
4
+ /**
5
+ * Expands a single user query into multiple semantic search variants
6
+ * to improve vector database recall (OpenClaw-style).
7
+ */
8
+ export async function expandQuery(query: string): Promise<string[]> {
9
+ const agents = loadAgents()
10
+ const settings = loadSettings()
11
+ const defaultAgent = agents[settings.defaultAgentId]
12
+ if (!defaultAgent) return [query]
13
+
14
+ const providerEntry = getProvider(defaultAgent.provider)
15
+ if (!providerEntry?.handler?.streamChat) return [query]
16
+
17
+ const creds = loadCredentials()
18
+ const cred = creds[defaultAgent.credentialId || '']
19
+ const apiKey = cred ? decryptKey(cred.encryptedKey) : undefined
20
+
21
+ const systemPrompt = `You are a search query expansion assistant.
22
+ Given a user's question, generate 3 different semantic search queries that would help find the answer in a vector database.
23
+ Use different vocabulary and focus on different aspects of the intent.
24
+ Format your response as a simple newline-separated list. No numbering, no bullets, no introduction.`
25
+
26
+ let expanded = ''
27
+ try {
28
+ await providerEntry.handler.streamChat({
29
+ session: { id: 'expansion', messages: [], model: defaultAgent.model, provider: defaultAgent.provider },
30
+ message: query,
31
+ apiKey,
32
+ systemPrompt,
33
+ write: (raw: string) => {
34
+ const lines = raw.split('\n').filter(Boolean)
35
+ for (const line of lines) {
36
+ if (!line.startsWith('data: ')) continue
37
+ try {
38
+ const ev = JSON.parse(line.slice(6))
39
+ if (ev.t === 'd' && ev.text) expanded += ev.text
40
+ } catch { /* skip */ }
41
+ }
42
+ },
43
+ active: new Map(),
44
+ loadHistory: () => [],
45
+ })
46
+
47
+ const variants = expanded.split('\n').map(l => l.trim()).filter(Boolean)
48
+ if (variants.length > 0) {
49
+ // Return original query + variants
50
+ return [query, ...variants.slice(0, 3)]
51
+ }
52
+ } catch (err) {
53
+ console.error('[query-expansion] Failed to expand query:', err)
54
+ }
55
+
56
+ return [query]
57
+ }
@@ -13,6 +13,7 @@ import { extractTaskResult, formatResultBody } from './task-result'
13
13
  import { getCheckpointSaver } from './langgraph-checkpoint'
14
14
  import { isProtectedMainSession } from './main-session'
15
15
  import { cascadeUnblock } from './dag-validation'
16
+ import { performGuardianRollback } from './guardian'
16
17
  import type { Agent, BoardTask, Connector, Message } from '@/types'
17
18
 
18
19
  // HMR-safe: pin processing flag to globalThis so hot reloads don't reset it
@@ -22,12 +23,14 @@ interface SessionMessageLike {
22
23
  role?: string
23
24
  text?: string
24
25
  time?: number
25
- kind?: 'chat' | 'heartbeat' | 'system' | 'context-clear'
26
+ kind?: string
26
27
  source?: {
27
28
  connectorId?: string
28
29
  channelId?: string
29
30
  }
30
31
  toolEvents?: Array<{ name?: string; output?: string }>
32
+ streaming?: boolean
33
+ imageUrl?: string
31
34
  }
32
35
 
33
36
  interface SessionLike {
@@ -96,6 +99,181 @@ function applyTaskPolicyDefaults(task: BoardTask): void {
96
99
  if (task.deadLetteredAt === undefined) task.deadLetteredAt = null
97
100
  }
98
101
 
102
+ const DEV_TASK_HINT = /\b(dev(?:\s+server)?|start(?:ing)?\s+(?:the\s+)?server|run(?:ning)?\s+(?:the\s+)?(?:app|project|site)|serve|localhost|http\s+server|web\s+server|npm\b|pnpm\b|yarn\b|bun\b|vite|next(?:\.js)?|react|build|compile)\b/i
103
+ const TASK_CWD_NOISE_DIRS = new Set([
104
+ 'uploads',
105
+ 'data',
106
+ 'projects',
107
+ 'tasks',
108
+ '.swarm-data-test',
109
+ '.git',
110
+ '.next',
111
+ 'node_modules',
112
+ ])
113
+ const PROJECT_MARKER_FILES = ['package.json', 'pyproject.toml', 'Cargo.toml', 'go.mod', '.git']
114
+ const SOURCE_MARKER_DIRS = ['src', 'app', 'public', 'pages']
115
+ const WORKSPACE_PROJECTS_DIR = path.join(WORKSPACE_DIR, 'projects')
116
+
117
+ interface WorkspaceDirCandidate {
118
+ dir: string
119
+ name: string
120
+ hasProjectMarker: boolean
121
+ hasSourceMarker: boolean
122
+ }
123
+
124
+ let workspaceDirCache: { expiresAt: number; candidates: WorkspaceDirCandidate[] } | null = null
125
+
126
+ function isExistingDirectory(dirPath: string): boolean {
127
+ try {
128
+ return fs.statSync(dirPath).isDirectory()
129
+ } catch {
130
+ return false
131
+ }
132
+ }
133
+
134
+ function isWithinDirectory(parent: string, child: string): boolean {
135
+ const parentResolved = path.resolve(parent)
136
+ const childResolved = path.resolve(child)
137
+ const rel = path.relative(parentResolved, childResolved)
138
+ return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel))
139
+ }
140
+
141
+ function normalizeForMatch(value: string): string {
142
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim()
143
+ }
144
+
145
+ function hasAnyMarker(dirPath: string, markers: string[]): boolean {
146
+ return markers.some((marker) => fs.existsSync(path.join(dirPath, marker)))
147
+ }
148
+
149
+ function normalizeDirCandidate(raw: unknown, baseDir: string): string | null {
150
+ if (typeof raw !== 'string') return null
151
+ const trimmed = raw.trim()
152
+ if (!trimmed) return null
153
+ const homeDir = process.env.HOME || ''
154
+ const expanded = trimmed === '~'
155
+ ? homeDir
156
+ : trimmed.startsWith('~/')
157
+ ? path.join(homeDir, trimmed.slice(2))
158
+ : trimmed
159
+ const resolved = path.isAbsolute(expanded) ? path.resolve(expanded) : path.resolve(baseDir, expanded)
160
+ return isExistingDirectory(resolved) ? resolved : null
161
+ }
162
+
163
+ function looksLikeDevTask(task: Pick<BoardTask, 'title' | 'description'>): boolean {
164
+ const text = `${task.title || ''} ${task.description || ''}`.trim()
165
+ return DEV_TASK_HINT.test(text)
166
+ }
167
+
168
+ function listWorkspaceDirCandidates(): WorkspaceDirCandidate[] {
169
+ const now = Date.now()
170
+ if (workspaceDirCache && workspaceDirCache.expiresAt > now) return workspaceDirCache.candidates
171
+
172
+ const candidates: WorkspaceDirCandidate[] = []
173
+ const seen = new Set<string>()
174
+ const roots = [WORKSPACE_DIR, WORKSPACE_PROJECTS_DIR]
175
+
176
+ for (const root of roots) {
177
+ if (!isExistingDirectory(root)) continue
178
+ let entries: fs.Dirent[] = []
179
+ try {
180
+ entries = fs.readdirSync(root, { withFileTypes: true })
181
+ } catch {
182
+ continue
183
+ }
184
+ for (const entry of entries) {
185
+ if (!entry.isDirectory()) continue
186
+ const name = entry.name
187
+ if (!name || name.startsWith('.')) continue
188
+ if (TASK_CWD_NOISE_DIRS.has(name)) continue
189
+ const dir = path.join(root, name)
190
+ const key = path.resolve(dir)
191
+ if (seen.has(key)) continue
192
+ seen.add(key)
193
+ candidates.push({
194
+ dir: key,
195
+ name,
196
+ hasProjectMarker: hasAnyMarker(key, PROJECT_MARKER_FILES),
197
+ hasSourceMarker: hasAnyMarker(key, SOURCE_MARKER_DIRS),
198
+ })
199
+ }
200
+ }
201
+
202
+ candidates.sort((a, b) => a.name.localeCompare(b.name))
203
+ workspaceDirCache = {
204
+ expiresAt: now + 15_000,
205
+ candidates,
206
+ }
207
+ return candidates
208
+ }
209
+
210
+ function inferWorkspaceProjectCwd(task: Pick<BoardTask, 'title' | 'description' | 'file'>): string | null {
211
+ const candidates = listWorkspaceDirCandidates()
212
+ if (!candidates.length) return null
213
+
214
+ const taskText = normalizeForMatch(`${task.title || ''} ${task.description || ''} ${task.file || ''}`)
215
+ const devTask = looksLikeDevTask(task)
216
+ const markerCandidates = candidates.filter((candidate) => candidate.hasProjectMarker)
217
+
218
+ let best: { dir: string; score: number } | null = null
219
+ for (const candidate of candidates) {
220
+ const nameNorm = normalizeForMatch(candidate.name)
221
+ if (!nameNorm) continue
222
+ let score = 0
223
+ if (taskText.includes(nameNorm)) score += 8
224
+ for (const token of nameNorm.split(' ')) {
225
+ if (token.length < 3) continue
226
+ if (taskText.includes(token)) score += 1
227
+ }
228
+ if (candidate.hasProjectMarker) score += devTask ? 3 : 1
229
+ if (candidate.hasSourceMarker) score += 1
230
+ if (!best || score > best.score) best = { dir: candidate.dir, score }
231
+ }
232
+
233
+ if (best && best.score >= 4) return best.dir
234
+ if (devTask && markerCandidates.length === 1) return markerCandidates[0].dir
235
+ return null
236
+ }
237
+
238
+ function resolveTaskExecutionCwd(task: ScheduleTaskMeta, sessions: Record<string, SessionLike>): string {
239
+ const workspaceRoot = path.resolve(WORKSPACE_DIR)
240
+
241
+ const explicitCwd = normalizeDirCandidate(task.cwd, workspaceRoot)
242
+ if (explicitCwd) return explicitCwd
243
+
244
+ const projectId = typeof task.projectId === 'string' ? task.projectId.trim() : ''
245
+ if (projectId) {
246
+ const projectDir = path.join(WORKSPACE_PROJECTS_DIR, projectId)
247
+ if (isExistingDirectory(projectDir)) return projectDir
248
+ }
249
+
250
+ const fileRef = typeof task.file === 'string' ? task.file.trim() : ''
251
+ if (fileRef) {
252
+ const filePath = path.isAbsolute(fileRef) ? fileRef : path.resolve(workspaceRoot, fileRef)
253
+ const fileDir = isExistingDirectory(filePath) ? filePath : path.dirname(filePath)
254
+ if (isExistingDirectory(fileDir) && isWithinDirectory(workspaceRoot, fileDir)) return fileDir
255
+ }
256
+
257
+ const inferredCwd = inferWorkspaceProjectCwd(task)
258
+ if (inferredCwd) return inferredCwd
259
+
260
+ const sourceSessionId = typeof task.createdInSessionId === 'string' ? task.createdInSessionId.trim() : ''
261
+ const sourceSessionCwd = sourceSessionId
262
+ ? normalizeDirCandidate(sessions[sourceSessionId]?.cwd, workspaceRoot)
263
+ : null
264
+ if (sourceSessionCwd && path.resolve(sourceSessionCwd) !== workspaceRoot) return sourceSessionCwd
265
+
266
+ const runSessionId = typeof task.sessionId === 'string' ? task.sessionId.trim() : ''
267
+ const runSessionCwd = runSessionId
268
+ ? normalizeDirCandidate(sessions[runSessionId]?.cwd, workspaceRoot)
269
+ : null
270
+ if (runSessionCwd && path.resolve(runSessionCwd) !== workspaceRoot) return runSessionCwd
271
+
272
+ const sandboxDir = path.join(workspaceRoot, 'tasks', task.id)
273
+ fs.mkdirSync(sandboxDir, { recursive: true })
274
+ return sandboxDir
275
+ }
276
+
99
277
  function queueContains(queue: string[], id: string): boolean {
100
278
  return queue.includes(id)
101
279
  }
@@ -315,12 +493,34 @@ export function resolveTaskOriginConnectorFollowupTarget(params: {
315
493
  // Task result extraction now uses Zod-validated structured data
316
494
  // from ./task-result.ts (extractTaskResult, formatResultBody)
317
495
 
496
+ /** Check if a task result looks incomplete (agent stopped mid-objective). */
497
+ function looksIncomplete(text: string): boolean {
498
+ if (!text) return false
499
+ const trimmed = text.trim()
500
+ // Ends with ellipsis or continuation signal
501
+ if (trimmed.endsWith('...') || trimmed.endsWith('…')) return true
502
+ // Ends with a step/phase header (agent was listing next steps)
503
+ if (/(?:^|\n)#{1,3}\s+(?:Step|Phase|Next)\s+\d/i.test(trimmed.slice(-200))) return true
504
+ // Contains forward-looking language at the end
505
+ const lastChunk = trimmed.slice(-300).toLowerCase()
506
+ if (/\b(?:next i(?:'ll| will)|now i(?:'ll| will)|let me (?:now|next)|moving on to|proceeding to)\b/.test(lastChunk)) return true
507
+ return false
508
+ }
509
+
318
510
  async function executeTaskRun(
319
511
  task: BoardTask,
320
512
  agent: Agent,
321
513
  sessionId: string,
322
514
  ): Promise<string> {
323
- const prompt = task.description || task.title
515
+ const basePrompt = task.description || task.title
516
+ const prompt = [
517
+ basePrompt,
518
+ '',
519
+ 'Completion requirements:',
520
+ '- Execute the task before replying; do not reply with only a plan.',
521
+ '- Include concrete evidence in your final summary: changed file paths, commands run, and verification results.',
522
+ '- If blocked, state the blocker explicitly and what input or permission is missing.',
523
+ ].join('\n')
324
524
  if (agent?.isOrchestrator) {
325
525
  return executeOrchestrator(agent, prompt, sessionId, task.id)
326
526
  }
@@ -330,11 +530,24 @@ async function executeTaskRun(
330
530
  message: prompt,
331
531
  internal: false,
332
532
  source: 'task',
533
+ runId: task.id,
333
534
  })
334
- const text = typeof run.text === 'string' ? run.text.trim() : ''
335
- if (text) return text
336
- if (run.error) return `Error: ${run.error}`
337
- return ''
535
+ let text = typeof run.text === 'string' ? run.text.trim() : ''
536
+ if (run.error) return text ? text : `Error: ${run.error}`
537
+
538
+ // Auto-continue if the result looks incomplete
539
+ if (text && looksIncomplete(text)) {
540
+ const followUp = await executeSessionChatTurn({
541
+ sessionId,
542
+ message: 'Continue and complete the remaining steps. Provide a final summary when done.',
543
+ internal: false,
544
+ source: 'task',
545
+ })
546
+ const contText = typeof followUp.text === 'string' ? followUp.text.trim() : ''
547
+ if (contText) text = contText
548
+ }
549
+
550
+ return text
338
551
  }
339
552
 
340
553
  function notifyMainChatScheduleResult(task: BoardTask): void {
@@ -377,8 +590,8 @@ function notifyMainChatScheduleResult(task: BoardTask): void {
377
590
  const now = Date.now()
378
591
  let changed = false
379
592
 
380
- const buildMsg = (): Message => {
381
- const msg: Message = { role: 'assistant', text: body, time: now, kind: 'system' }
593
+ const buildMsg = (): SessionMessageLike => {
594
+ const msg: SessionMessageLike = { role: 'assistant', text: body, time: now, kind: 'system' }
382
595
  if (firstImage) msg.imageUrl = firstImage.url
383
596
  return msg
384
597
  }
@@ -701,6 +914,7 @@ export function enqueueTask(taskId: string) {
701
914
  export function validateCompletedTasksQueue() {
702
915
  const tasks = loadTasks()
703
916
  const sessions = loadSessions()
917
+ const settings = loadSettings()
704
918
  const now = Date.now()
705
919
  let checked = 0
706
920
  let demoted = 0
@@ -717,7 +931,7 @@ export function validateCompletedTasksQueue() {
717
931
  tasksDirty = true
718
932
  }
719
933
 
720
- const validation = validateTaskCompletion(task, { report })
934
+ const validation = validateTaskCompletion(task, { report, settings })
721
935
  const prevValidation = task.validation || null
722
936
  const validationChanged = !prevValidation
723
937
  || prevValidation.ok !== validation.ok
@@ -802,6 +1016,32 @@ function scheduleRetryOrDeadLetter(task: BoardTask, reason: string): 'retry' | '
802
1016
  text: `Task moved to dead-letter after ${task.attempts}/${task.maxAttempts} attempts.\n\nReason: ${reason}`,
803
1017
  createdAt: now,
804
1018
  })
1019
+
1020
+ // Guardian Auto-Rollback
1021
+ const agents = loadAgents()
1022
+ const agent = task.agentId ? agents[task.agentId] : null
1023
+ if (agent?.autoRecovery) {
1024
+ const cwd = task.projectId
1025
+ ? path.join(WORKSPACE_DIR, 'projects', task.projectId)
1026
+ : WORKSPACE_DIR
1027
+ const rollback = performGuardianRollback(cwd)
1028
+ if (rollback.ok) {
1029
+ task.comments.push({
1030
+ id: genId(),
1031
+ author: 'Guardian',
1032
+ text: `Auto-recovery triggered: Workspace successfully rolled back to last clean state.`,
1033
+ createdAt: now + 1,
1034
+ })
1035
+ } else {
1036
+ task.comments.push({
1037
+ id: genId(),
1038
+ author: 'Guardian',
1039
+ text: `Auto-recovery failed: ${rollback.reason}`,
1040
+ createdAt: now + 1,
1041
+ })
1042
+ }
1043
+ }
1044
+
805
1045
  return 'dead_lettered'
806
1046
  }
807
1047
 
@@ -903,7 +1143,9 @@ export async function processNext() {
903
1143
  task.validation = null
904
1144
  task.updatedAt = Date.now()
905
1145
 
906
- const taskCwd = task.cwd || WORKSPACE_DIR
1146
+ const sessionsForCwd = loadSessions() as Record<string, SessionLike>
1147
+ const taskCwd = resolveTaskExecutionCwd(task as ScheduleTaskMeta, sessionsForCwd)
1148
+ task.cwd = taskCwd
907
1149
  let sessionId = ''
908
1150
  const scheduleTask = task as ScheduleTaskMeta
909
1151
  const isScheduleTask = scheduleTask.sourceType === 'schedule'
@@ -1002,6 +1244,7 @@ export async function processNext() {
1002
1244
  try {
1003
1245
  const result = await executeTaskRun(task, agent, sessionId)
1004
1246
  const t2 = loadTasks()
1247
+ const settings = loadSettings()
1005
1248
  if (t2[taskId]) {
1006
1249
  applyTaskPolicyDefaults(t2[taskId])
1007
1250
  // Structured extraction: Zod-validated result with typed artifacts
@@ -1018,7 +1261,7 @@ export async function processNext() {
1018
1261
  t2[taskId].updatedAt = Date.now()
1019
1262
  const report = ensureTaskCompletionReport(t2[taskId])
1020
1263
  if (report?.relativePath) t2[taskId].completionReportPath = report.relativePath
1021
- const validation = validateTaskCompletion(t2[taskId], { report })
1264
+ const validation = validateTaskCompletion(t2[taskId], { report, settings })
1022
1265
  t2[taskId].validation = validation
1023
1266
 
1024
1267
  const now = Date.now()
@@ -1233,6 +1476,29 @@ export function recoverStalledRunningTasks(): { recovered: number; deadLettered:
1233
1476
 
1234
1477
  for (const task of Object.values(tasks) as BoardTask[]) {
1235
1478
  if (task.status !== 'running') continue
1479
+ if (!task.startedAt) {
1480
+ const recoveredAt = Date.now()
1481
+ task.status = 'queued'
1482
+ task.queuedAt = task.queuedAt || recoveredAt
1483
+ task.retryScheduledAt = null
1484
+ task.updatedAt = recoveredAt
1485
+ task.error = 'Recovered inconsistent running state (missing startedAt); requeued.'
1486
+ if (!task.comments) task.comments = []
1487
+ task.comments.push({
1488
+ id: genId(),
1489
+ author: 'System',
1490
+ text: 'Recovered inconsistent running state (missing startedAt). Task requeued.',
1491
+ createdAt: recoveredAt,
1492
+ })
1493
+ pushQueueUnique(queue, task.id)
1494
+ recovered++
1495
+ changed = true
1496
+ pushMainLoopEventToMainSessions({
1497
+ type: 'task_stall_recovered',
1498
+ text: `Recovered inconsistent running task "${task.title}" (${task.id}) and requeued it.`,
1499
+ })
1500
+ continue
1501
+ }
1236
1502
  const since = Math.max(task.updatedAt || 0, task.startedAt || 0)
1237
1503
  if (!since || (now - since) < staleMs) continue
1238
1504
 
@@ -1259,6 +1525,9 @@ export function recoverStalledRunningTasks(): { recovered: number; deadLettered:
1259
1525
  if (changed) {
1260
1526
  saveTasks(tasks)
1261
1527
  saveQueue(queue)
1528
+ if (recovered > 0) {
1529
+ setTimeout(() => processNext(), 250)
1530
+ }
1262
1531
  }
1263
1532
 
1264
1533
  return { recovered, deadLettered }
@@ -3,6 +3,7 @@ import {
3
3
  DEFAULT_AGENT_LOOP_RECURSION_LIMIT,
4
4
  DEFAULT_CLAUDE_CODE_TIMEOUT_SEC,
5
5
  DEFAULT_CLI_PROCESS_TIMEOUT_SEC,
6
+ DEFAULT_DELEGATION_MAX_DEPTH,
6
7
  DEFAULT_LEGACY_ORCHESTRATOR_MAX_TURNS,
7
8
  DEFAULT_LOOP_MODE,
8
9
  DEFAULT_ONGOING_LOOP_MAX_ITERATIONS,
@@ -17,6 +18,7 @@ export interface RuntimeSettings {
17
18
  agentLoopRecursionLimit: number
18
19
  orchestratorLoopRecursionLimit: number
19
20
  legacyOrchestratorMaxTurns: number
21
+ delegationMaxDepth: number
20
22
  ongoingLoopMaxIterations: number
21
23
  ongoingLoopMaxRuntimeMs: number | null
22
24
  shellCommandTimeoutMs: number
@@ -61,6 +63,12 @@ export function loadRuntimeSettings(): RuntimeSettings {
61
63
  1,
62
64
  300,
63
65
  )
66
+ const delegationMaxDepth = parseIntSetting(
67
+ settings.delegationMaxDepth,
68
+ DEFAULT_DELEGATION_MAX_DEPTH,
69
+ 1,
70
+ 12,
71
+ )
64
72
  const ongoingLoopMaxIterations = parseIntSetting(
65
73
  settings.ongoingLoopMaxIterations,
66
74
  DEFAULT_ONGOING_LOOP_MAX_ITERATIONS,
@@ -98,6 +106,7 @@ export function loadRuntimeSettings(): RuntimeSettings {
98
106
  agentLoopRecursionLimit,
99
107
  orchestratorLoopRecursionLimit,
100
108
  legacyOrchestratorMaxTurns,
109
+ delegationMaxDepth,
101
110
  ongoingLoopMaxIterations,
102
111
  ongoingLoopMaxRuntimeMs: ongoingLoopMaxRuntimeMinutes > 0 ? ongoingLoopMaxRuntimeMinutes * 60_000 : null,
103
112
  shellCommandTimeoutMs: shellCommandTimeoutSec * 1000,
@@ -0,0 +1,23 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { isMainMissionSession } from './session-run-manager'
4
+
5
+ describe('isMainMissionSession', () => {
6
+ it('accepts explicit main sessions', () => {
7
+ assert.equal(isMainMissionSession({ id: 'main-user', name: '__main__' }), true)
8
+ })
9
+
10
+ it('rejects human agent-thread sessions', () => {
11
+ assert.equal(
12
+ isMainMissionSession({ id: 'agent-thread-agent_coder-123', name: 'agent-thread:agent_coder', sessionType: 'human' }),
13
+ false,
14
+ )
15
+ })
16
+
17
+ it('accepts orchestrated sessions', () => {
18
+ assert.equal(
19
+ isMainMissionSession({ id: 'agent-thread-worker-1', name: 'agent-thread:worker', sessionType: 'orchestrated' }),
20
+ true,
21
+ )
22
+ })
23
+ })
@@ -203,7 +203,7 @@ function scheduleMainLoopFollowup(sessionId: string, followup: MainLoopFollowupR
203
203
  try {
204
204
  const sessions = loadSessions()
205
205
  const session = sessions[sessionId]
206
- if (!session || session.name !== '__main__') return
206
+ if (!session || !isMainMissionSession(session)) return
207
207
  enqueueSessionRun({
208
208
  sessionId,
209
209
  message: followup.message,
@@ -218,6 +218,16 @@ function scheduleMainLoopFollowup(sessionId: string, followup: MainLoopFollowupR
218
218
  }, delayMs)
219
219
  }
220
220
 
221
+ export function isMainMissionSession(session: Record<string, unknown>): boolean {
222
+ const id = typeof session.id === 'string' ? session.id.trim() : ''
223
+ const name = typeof session.name === 'string' ? session.name.trim() : ''
224
+ const sessionType = typeof session.sessionType === 'string' ? session.sessionType : ''
225
+ if (id.startsWith('main-') || name === '__main__') return true
226
+ // Only orchestrated thread sessions should receive autonomous main-loop followups.
227
+ if (sessionType === 'orchestrated') return true
228
+ return false
229
+ }
230
+
221
231
  async function drainExecution(executionKey: string): Promise<void> {
222
232
  if (state.runningByExecution.has(executionKey)) return
223
233
  const q = queueForExecution(executionKey)
@@ -271,6 +281,9 @@ async function drainExecution(executionKey: string): Promise<void> {
271
281
  resultText: result.text,
272
282
  error: result.error,
273
283
  toolEvents: result.toolEvents,
284
+ inputTokens: result.inputTokens,
285
+ outputTokens: result.outputTokens,
286
+ estimatedCost: result.estimatedCost,
274
287
  })
275
288
  } catch (mainLoopErr: any) {
276
289
  log.warn('session-run', `Main-loop update failed for ${next.run.id}`, mainLoopErr?.message || String(mainLoopErr))
@@ -372,6 +385,19 @@ export interface EnqueueSessionRunResult {
372
385
  abort: () => void
373
386
  }
374
387
 
388
+ const LONG_TOOL_NAMES: ReadonlySet<string> = new Set(['claude_code', 'codex_cli', 'opencode_cli'])
389
+
390
+ function computeEffectiveRunTimeoutMs(
391
+ baseTimeoutMs: number,
392
+ sessionTools: string[],
393
+ runtime: { claudeCodeTimeoutMs: number },
394
+ ): number {
395
+ const hasLongTool = sessionTools.some(t => LONG_TOOL_NAMES.has(t))
396
+ if (!hasLongTool) return baseTimeoutMs
397
+ const toolTimeout = runtime.claudeCodeTimeoutMs + 120_000
398
+ return Math.max(baseTimeoutMs, toolTimeout)
399
+ }
400
+
375
401
  export function enqueueSessionRun(input: EnqueueSessionRunInput): EnqueueSessionRunResult {
376
402
  const internal = input.internal === true
377
403
  const mode = normalizeMode(input.mode, internal)
@@ -379,9 +405,13 @@ export function enqueueSessionRun(input: EnqueueSessionRunInput): EnqueueSession
379
405
  const executionKey = executionKeyForSession(input.sessionId)
380
406
  const runtime = loadRuntimeSettings()
381
407
  const defaultMaxRuntimeMs = runtime.ongoingLoopMaxRuntimeMs ?? (10 * 60_000)
408
+ const sessions = loadSessions()
409
+ const sessionData = sessions[input.sessionId]
410
+ const sessionTools: string[] = sessionData?.tools || []
411
+ const adjustedDefaultMs = computeEffectiveRunTimeoutMs(defaultMaxRuntimeMs, sessionTools, runtime)
382
412
  const effectiveMaxRuntimeMs = typeof input.maxRuntimeMs === 'number'
383
413
  ? input.maxRuntimeMs
384
- : defaultMaxRuntimeMs
414
+ : adjustedDefaultMs
385
415
 
386
416
  const dedupe = findDedupeMatch(input.sessionId, input.dedupeKey)
387
417
  if (dedupe) {