@swarmclawai/swarmclaw 0.7.6 → 0.7.8
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.
- package/README.md +19 -10
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +16 -0
- package/src/app/api/agents/route.ts +2 -0
- package/src/app/api/chats/[id]/route.ts +21 -1
- package/src/app/api/chats/route.ts +13 -1
- package/src/app/api/connectors/[id]/route.ts +20 -2
- package/src/app/api/connectors/route.ts +12 -8
- package/src/app/api/external-agents/[id]/heartbeat/route.ts +3 -0
- package/src/app/api/external-agents/[id]/route.ts +38 -6
- package/src/app/api/external-agents/route.ts +17 -1
- package/src/app/api/gateways/[id]/health/route.ts +8 -0
- package/src/app/api/gateways/[id]/route.ts +53 -1
- package/src/app/api/gateways/route.ts +53 -0
- package/src/app/api/openclaw/deploy/route.ts +139 -0
- package/src/app/api/projects/[id]/route.ts +6 -2
- package/src/app/api/projects/route.ts +4 -3
- package/src/app/api/secrets/[id]/route.ts +1 -0
- package/src/app/api/secrets/route.ts +2 -1
- package/src/app/api/settings/route.ts +2 -0
- package/src/cli/index.js +40 -0
- package/src/cli/index.test.js +68 -0
- package/src/cli/spec.js +60 -0
- package/src/components/agents/agent-sheet.tsx +281 -33
- package/src/components/auth/setup-wizard.tsx +75 -2
- package/src/components/chat/chat-area.tsx +36 -19
- package/src/components/chat/chat-header.tsx +4 -0
- package/src/components/chat/delegation-banner.test.ts +14 -1
- package/src/components/chat/delegation-banner.tsx +1 -1
- package/src/components/gateways/gateway-sheet.tsx +140 -8
- package/src/components/layout/app-layout.tsx +40 -23
- package/src/components/openclaw/openclaw-deploy-panel.tsx +591 -9
- package/src/components/projects/project-detail.tsx +217 -0
- package/src/components/projects/project-sheet.tsx +176 -4
- package/src/components/providers/provider-list.tsx +221 -17
- package/src/components/shared/settings/section-capability-policy.tsx +38 -0
- package/src/components/shared/settings/section-voice.tsx +11 -3
- package/src/components/tasks/approvals-panel.tsx +177 -18
- package/src/components/tasks/task-board.tsx +137 -23
- package/src/components/tasks/task-card.tsx +29 -0
- package/src/components/tasks/task-sheet.tsx +16 -4
- package/src/lib/server/agent-runtime-config.ts +142 -7
- package/src/lib/server/agent-thread-session.ts +9 -1
- package/src/lib/server/capability-router.test.ts +22 -0
- package/src/lib/server/capability-router.ts +54 -18
- package/src/lib/server/chat-execution.ts +33 -3
- package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
- package/src/lib/server/connectors/manager.ts +99 -74
- package/src/lib/server/daemon-state.ts +83 -46
- package/src/lib/server/elevenlabs.test.ts +59 -1
- package/src/lib/server/heartbeat-service.ts +5 -1
- package/src/lib/server/main-agent-loop.test.ts +260 -0
- package/src/lib/server/main-agent-loop.ts +559 -14
- package/src/lib/server/openclaw-deploy.test.ts +8 -0
- package/src/lib/server/openclaw-deploy.ts +679 -19
- package/src/lib/server/orchestrator-lg.ts +1 -0
- package/src/lib/server/orchestrator.ts +11 -0
- package/src/lib/server/plugins.ts +6 -1
- package/src/lib/server/project-context.ts +162 -0
- package/src/lib/server/project-utils.ts +150 -0
- package/src/lib/server/queue-followups.test.ts +147 -2
- package/src/lib/server/queue.ts +278 -8
- package/src/lib/server/session-run-manager.ts +31 -0
- package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
- package/src/lib/server/session-tools/connector.ts +26 -1
- package/src/lib/server/session-tools/context.ts +5 -0
- package/src/lib/server/session-tools/crud.ts +265 -76
- package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
- package/src/lib/server/session-tools/delegate.ts +38 -2
- package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
- package/src/lib/server/session-tools/memory.ts +14 -2
- package/src/lib/server/session-tools/platform-access.test.ts +58 -0
- package/src/lib/server/session-tools/platform.ts +60 -19
- package/src/lib/server/session-tools/web-inputs.test.ts +17 -0
- package/src/lib/server/session-tools/web.ts +153 -6
- package/src/lib/server/stream-agent-chat.test.ts +27 -2
- package/src/lib/server/stream-agent-chat.ts +104 -30
- package/src/lib/server/tool-aliases.ts +2 -0
- package/src/lib/server/tool-capability-policy.test.ts +24 -0
- package/src/lib/server/tool-capability-policy.ts +29 -1
- package/src/lib/server/tool-planning.test.ts +44 -0
- package/src/lib/server/tool-planning.ts +269 -0
- package/src/lib/setup-defaults.ts +2 -2
- package/src/lib/tool-definitions.ts +2 -1
- package/src/lib/validation/schemas.ts +9 -0
- package/src/types/index.ts +104 -0
package/src/lib/server/queue.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { extractTaskResult, formatResultBody } from './task-result'
|
|
|
13
13
|
import { getCheckpointSaver } from './langgraph-checkpoint'
|
|
14
14
|
import { cascadeUnblock } from './dag-validation'
|
|
15
15
|
import { performGuardianRollback } from './guardian'
|
|
16
|
-
import type { Agent, BoardTask, Connector, Message } from '@/types'
|
|
16
|
+
import type { Agent, BoardTask, Connector, Message, Session } from '@/types'
|
|
17
17
|
|
|
18
18
|
// HMR-safe: pin processing flag to globalThis so hot reloads don't reset it
|
|
19
19
|
const _queueState = ((globalThis as Record<string, unknown>).__swarmclaw_queue__ ??= { processing: false, pendingKick: false }) as { processing: boolean; pendingKick: boolean }
|
|
@@ -80,6 +80,36 @@ function normalizeInt(value: unknown, fallback: number, min: number, max: number
|
|
|
80
80
|
return Math.max(min, Math.min(max, Math.trunc(parsed)))
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
const OPENCLAW_USE_CASE_TAGS = new Set([
|
|
84
|
+
'local-dev',
|
|
85
|
+
'single-vps',
|
|
86
|
+
'private-tailnet',
|
|
87
|
+
'browser-heavy',
|
|
88
|
+
'team-control',
|
|
89
|
+
])
|
|
90
|
+
|
|
91
|
+
function deriveTaskRoutePreferences(task: BoardTask): {
|
|
92
|
+
preferredGatewayTags?: string[]
|
|
93
|
+
preferredGatewayUseCase?: string | null
|
|
94
|
+
} {
|
|
95
|
+
const tags = Array.isArray(task.tags)
|
|
96
|
+
? [...new Set(task.tags.map((tag) => (typeof tag === 'string' ? tag.trim().toLowerCase() : '')).filter(Boolean))]
|
|
97
|
+
: []
|
|
98
|
+
const customUseCase = typeof task.customFields?.openclawUseCase === 'string'
|
|
99
|
+
? task.customFields.openclawUseCase
|
|
100
|
+
: typeof task.customFields?.gatewayUseCase === 'string'
|
|
101
|
+
? task.customFields.gatewayUseCase
|
|
102
|
+
: null
|
|
103
|
+
const preferredGatewayUseCase = customUseCase && OPENCLAW_USE_CASE_TAGS.has(customUseCase)
|
|
104
|
+
? customUseCase
|
|
105
|
+
: (tags.find((tag) => OPENCLAW_USE_CASE_TAGS.has(tag)) || null)
|
|
106
|
+
const preferredGatewayTags = tags.filter((tag) => tag !== preferredGatewayUseCase)
|
|
107
|
+
return {
|
|
108
|
+
preferredGatewayTags,
|
|
109
|
+
preferredGatewayUseCase,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
83
113
|
function resolveTaskPolicy(task: BoardTask): { maxAttempts: number; backoffSec: number } {
|
|
84
114
|
const settings = loadSettings()
|
|
85
115
|
const defaultMaxAttempts = normalizeInt(settings.defaultTaskMaxAttempts, 3, 1, 20)
|
|
@@ -98,6 +128,212 @@ function applyTaskPolicyDefaults(task: BoardTask): void {
|
|
|
98
128
|
if (task.deadLetteredAt === undefined) task.deadLetteredAt = null
|
|
99
129
|
}
|
|
100
130
|
|
|
131
|
+
export interface TaskResumeState {
|
|
132
|
+
claudeSessionId: string | null
|
|
133
|
+
codexThreadId: string | null
|
|
134
|
+
opencodeSessionId: string | null
|
|
135
|
+
delegateResumeIds: NonNullable<Session['delegateResumeIds']>
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface TaskResumeContext {
|
|
139
|
+
source: 'self' | 'delegated_from_task' | 'blocked_by'
|
|
140
|
+
sourceTaskId: string
|
|
141
|
+
sourceTaskTitle: string
|
|
142
|
+
sourceSessionId: string | null
|
|
143
|
+
resume: TaskResumeState
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function normalizeResumeHandle(value: unknown): string | null {
|
|
147
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function buildEmptyDelegateResumeIds(): NonNullable<Session['delegateResumeIds']> {
|
|
151
|
+
return {
|
|
152
|
+
claudeCode: null,
|
|
153
|
+
codex: null,
|
|
154
|
+
opencode: null,
|
|
155
|
+
gemini: null,
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function normalizeCliProvider(value: unknown): string | null {
|
|
160
|
+
return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : null
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function hasResumeState(state: TaskResumeState | null | undefined): state is TaskResumeState {
|
|
164
|
+
if (!state) return false
|
|
165
|
+
return Boolean(
|
|
166
|
+
state.claudeSessionId
|
|
167
|
+
|| state.codexThreadId
|
|
168
|
+
|| state.opencodeSessionId
|
|
169
|
+
|| state.delegateResumeIds.claudeCode
|
|
170
|
+
|| state.delegateResumeIds.codex
|
|
171
|
+
|| state.delegateResumeIds.opencode
|
|
172
|
+
|| state.delegateResumeIds.gemini
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function extractTaskResumeState(task: Partial<BoardTask> | null | undefined): TaskResumeState | null {
|
|
177
|
+
if (!task) return null
|
|
178
|
+
|
|
179
|
+
const legacyResumeId = normalizeResumeHandle(task.cliResumeId)
|
|
180
|
+
const legacyProvider = normalizeCliProvider(task.cliProvider)
|
|
181
|
+
const claudeSessionId = normalizeResumeHandle(task.claudeResumeId)
|
|
182
|
+
|| (legacyProvider === 'claude-cli' ? legacyResumeId : null)
|
|
183
|
+
const codexThreadId = normalizeResumeHandle(task.codexResumeId)
|
|
184
|
+
|| (legacyProvider === 'codex-cli' ? legacyResumeId : null)
|
|
185
|
+
const opencodeSessionId = normalizeResumeHandle(task.opencodeResumeId)
|
|
186
|
+
|| (legacyProvider === 'opencode-cli' ? legacyResumeId : null)
|
|
187
|
+
const geminiSessionId = normalizeResumeHandle(task.geminiResumeId)
|
|
188
|
+
|| (legacyProvider === 'gemini-cli' ? legacyResumeId : null)
|
|
189
|
+
|
|
190
|
+
const resume = {
|
|
191
|
+
claudeSessionId,
|
|
192
|
+
codexThreadId,
|
|
193
|
+
opencodeSessionId,
|
|
194
|
+
delegateResumeIds: {
|
|
195
|
+
claudeCode: claudeSessionId,
|
|
196
|
+
codex: codexThreadId,
|
|
197
|
+
opencode: opencodeSessionId,
|
|
198
|
+
gemini: geminiSessionId,
|
|
199
|
+
},
|
|
200
|
+
} satisfies TaskResumeState
|
|
201
|
+
|
|
202
|
+
return hasResumeState(resume) ? resume : null
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function extractSessionResumeState(session: Partial<Session> | null | undefined): TaskResumeState | null {
|
|
206
|
+
if (!session) return null
|
|
207
|
+
|
|
208
|
+
const claudeSessionId = normalizeResumeHandle(session.claudeSessionId)
|
|
209
|
+
const codexThreadId = normalizeResumeHandle(session.codexThreadId)
|
|
210
|
+
const opencodeSessionId = normalizeResumeHandle(session.opencodeSessionId)
|
|
211
|
+
const delegateResumeIds = session.delegateResumeIds && typeof session.delegateResumeIds === 'object'
|
|
212
|
+
? { ...buildEmptyDelegateResumeIds(), ...session.delegateResumeIds }
|
|
213
|
+
: buildEmptyDelegateResumeIds()
|
|
214
|
+
|
|
215
|
+
const resume = {
|
|
216
|
+
claudeSessionId,
|
|
217
|
+
codexThreadId,
|
|
218
|
+
opencodeSessionId,
|
|
219
|
+
delegateResumeIds: {
|
|
220
|
+
claudeCode: normalizeResumeHandle(delegateResumeIds.claudeCode) || claudeSessionId,
|
|
221
|
+
codex: normalizeResumeHandle(delegateResumeIds.codex) || codexThreadId,
|
|
222
|
+
opencode: normalizeResumeHandle(delegateResumeIds.opencode) || opencodeSessionId,
|
|
223
|
+
gemini: normalizeResumeHandle(delegateResumeIds.gemini),
|
|
224
|
+
},
|
|
225
|
+
} satisfies TaskResumeState
|
|
226
|
+
|
|
227
|
+
return hasResumeState(resume) ? resume : null
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function resolveTaskResumeContext(
|
|
231
|
+
task: BoardTask,
|
|
232
|
+
tasksById: Record<string, BoardTask>,
|
|
233
|
+
sessionsById?: Record<string, SessionLike | Session>,
|
|
234
|
+
): TaskResumeContext | null {
|
|
235
|
+
const candidates: Array<{ source: TaskResumeContext['source']; taskId: string | null | undefined }> = [
|
|
236
|
+
{ source: 'self', taskId: task.id },
|
|
237
|
+
{ source: 'delegated_from_task', taskId: task.delegatedFromTaskId },
|
|
238
|
+
...((Array.isArray(task.blockedBy) ? task.blockedBy : []).map((taskId) => ({ source: 'blocked_by' as const, taskId }))),
|
|
239
|
+
]
|
|
240
|
+
const seen = new Set<string>()
|
|
241
|
+
|
|
242
|
+
for (const candidate of candidates) {
|
|
243
|
+
const taskId = typeof candidate.taskId === 'string' ? candidate.taskId.trim() : ''
|
|
244
|
+
if (!taskId || seen.has(taskId)) continue
|
|
245
|
+
seen.add(taskId)
|
|
246
|
+
const sourceTask = taskId === task.id ? task : tasksById[taskId]
|
|
247
|
+
if (!sourceTask) continue
|
|
248
|
+
const sourceSessionId = normalizeResumeHandle(sourceTask.checkpoint?.lastSessionId) || normalizeResumeHandle(sourceTask.sessionId)
|
|
249
|
+
const resume = extractTaskResumeState(sourceTask)
|
|
250
|
+
|| (sourceSessionId && sessionsById?.[sourceSessionId]
|
|
251
|
+
? extractSessionResumeState(sessionsById[sourceSessionId] as Session)
|
|
252
|
+
: null)
|
|
253
|
+
if (!resume) continue
|
|
254
|
+
return {
|
|
255
|
+
source: candidate.source,
|
|
256
|
+
sourceTaskId: sourceTask.id,
|
|
257
|
+
sourceTaskTitle: sourceTask.title,
|
|
258
|
+
sourceSessionId,
|
|
259
|
+
resume,
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return null
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function applyTaskResumeStateToSession(session: Session, resume: TaskResumeState | null | undefined): boolean {
|
|
267
|
+
if (!hasResumeState(resume)) return false
|
|
268
|
+
|
|
269
|
+
let changed = false
|
|
270
|
+
const directFields: Array<['claudeSessionId' | 'codexThreadId' | 'opencodeSessionId', string | null]> = [
|
|
271
|
+
['claudeSessionId', resume.claudeSessionId],
|
|
272
|
+
['codexThreadId', resume.codexThreadId],
|
|
273
|
+
['opencodeSessionId', resume.opencodeSessionId],
|
|
274
|
+
]
|
|
275
|
+
for (const [key, value] of directFields) {
|
|
276
|
+
if (!value || session[key] === value) continue
|
|
277
|
+
session[key] = value
|
|
278
|
+
changed = true
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const currentDelegateResume = session.delegateResumeIds && typeof session.delegateResumeIds === 'object'
|
|
282
|
+
? { ...buildEmptyDelegateResumeIds(), ...session.delegateResumeIds }
|
|
283
|
+
: buildEmptyDelegateResumeIds()
|
|
284
|
+
for (const [key, value] of Object.entries(resume.delegateResumeIds) as Array<[keyof NonNullable<Session['delegateResumeIds']>, string | null]>) {
|
|
285
|
+
if (!value || currentDelegateResume[key] === value) continue
|
|
286
|
+
currentDelegateResume[key] = value
|
|
287
|
+
changed = true
|
|
288
|
+
}
|
|
289
|
+
if (changed) session.delegateResumeIds = currentDelegateResume
|
|
290
|
+
return changed
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function resolveReusableTaskSessionId(
|
|
294
|
+
task: BoardTask,
|
|
295
|
+
tasks: Record<string, BoardTask>,
|
|
296
|
+
sessions: Record<string, SessionLike>,
|
|
297
|
+
): string {
|
|
298
|
+
const candidateTaskIds = [
|
|
299
|
+
task.id,
|
|
300
|
+
typeof task.delegatedFromTaskId === 'string' ? task.delegatedFromTaskId : '',
|
|
301
|
+
...(Array.isArray(task.blockedBy) ? task.blockedBy : []),
|
|
302
|
+
]
|
|
303
|
+
const seen = new Set<string>()
|
|
304
|
+
for (const candidateTaskId of candidateTaskIds) {
|
|
305
|
+
const taskId = typeof candidateTaskId === 'string' ? candidateTaskId.trim() : ''
|
|
306
|
+
if (!taskId || seen.has(taskId)) continue
|
|
307
|
+
seen.add(taskId)
|
|
308
|
+
const sourceTask = taskId === task.id ? task : tasks[taskId]
|
|
309
|
+
if (!sourceTask) continue
|
|
310
|
+
const candidates = [
|
|
311
|
+
normalizeResumeHandle(sourceTask.checkpoint?.lastSessionId),
|
|
312
|
+
normalizeResumeHandle(sourceTask.sessionId),
|
|
313
|
+
]
|
|
314
|
+
for (const candidate of candidates) {
|
|
315
|
+
if (candidate && sessions[candidate]) return candidate
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return ''
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function buildTaskContinuationNote(
|
|
322
|
+
reusedExistingSession: boolean,
|
|
323
|
+
resumeContext: TaskResumeContext | null,
|
|
324
|
+
): string {
|
|
325
|
+
const notes: string[] = []
|
|
326
|
+
if (reusedExistingSession) {
|
|
327
|
+
notes.push('Reusing the previous execution session for this task.')
|
|
328
|
+
}
|
|
329
|
+
if (resumeContext?.source === 'delegated_from_task' || resumeContext?.source === 'blocked_by') {
|
|
330
|
+
notes.push(`Stored CLI context is available from related task "${resumeContext.sourceTaskTitle}".`)
|
|
331
|
+
} else if (resumeContext?.source === 'self' && !reusedExistingSession) {
|
|
332
|
+
notes.push('Stored CLI resume handles are available for continuation.')
|
|
333
|
+
}
|
|
334
|
+
return notes.length ? `\n\n${notes.join(' ')}` : ''
|
|
335
|
+
}
|
|
336
|
+
|
|
101
337
|
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
|
|
102
338
|
const TASK_CWD_NOISE_DIRS = new Set([
|
|
103
339
|
'uploads',
|
|
@@ -1027,7 +1263,7 @@ function scheduleRetryOrDeadLetter(task: BoardTask, reason: string): 'retry' | '
|
|
|
1027
1263
|
return 'dead_lettered'
|
|
1028
1264
|
}
|
|
1029
1265
|
|
|
1030
|
-
function dequeueNextRunnableTask(queue: string[], tasks: Record<string, BoardTask>): string | null {
|
|
1266
|
+
export function dequeueNextRunnableTask(queue: string[], tasks: Record<string, BoardTask>): string | null {
|
|
1031
1267
|
const now = Date.now()
|
|
1032
1268
|
|
|
1033
1269
|
// Remove stale entries first.
|
|
@@ -1041,7 +1277,9 @@ function dequeueNextRunnableTask(queue: string[], tasks: Record<string, BoardTas
|
|
|
1041
1277
|
const task = tasks[id]
|
|
1042
1278
|
if (!task) return false
|
|
1043
1279
|
const retryAt = typeof task.retryScheduledAt === 'number' ? task.retryScheduledAt : null
|
|
1044
|
-
|
|
1280
|
+
if (retryAt && retryAt > now) return false
|
|
1281
|
+
const blockers = Array.isArray(task.blockedBy) ? task.blockedBy : []
|
|
1282
|
+
return blockers.every((blockerId) => tasks[blockerId]?.status === 'completed')
|
|
1045
1283
|
})
|
|
1046
1284
|
if (idx === -1) return null
|
|
1047
1285
|
const [taskId] = queue.splice(idx, 1)
|
|
@@ -1134,9 +1372,12 @@ export async function processNext() {
|
|
|
1134
1372
|
const sourceScheduleId = typeof scheduleTask.sourceScheduleId === 'string'
|
|
1135
1373
|
? scheduleTask.sourceScheduleId
|
|
1136
1374
|
: ''
|
|
1375
|
+
const reusableTaskSessionId = resolveReusableTaskSessionId(task, tasks as Record<string, BoardTask>, sessionsForCwd)
|
|
1376
|
+
const resumeContext = resolveTaskResumeContext(task, tasks as Record<string, BoardTask>, sessionsForCwd as Record<string, SessionLike | Session>)
|
|
1137
1377
|
|
|
1138
1378
|
// Resolve the agent's persistent thread session to use as parentSessionId
|
|
1139
1379
|
const agentThreadSessionId = agent.threadSessionId || null
|
|
1380
|
+
const taskRoutePreferences = deriveTaskRoutePreferences(task)
|
|
1140
1381
|
|
|
1141
1382
|
if (isScheduleTask && sourceScheduleId) {
|
|
1142
1383
|
const schedules = loadSchedules()
|
|
@@ -1151,7 +1392,13 @@ export async function processNext() {
|
|
|
1151
1392
|
}
|
|
1152
1393
|
}
|
|
1153
1394
|
if (!sessionId) {
|
|
1154
|
-
sessionId = createOrchestratorSession(
|
|
1395
|
+
sessionId = createOrchestratorSession(
|
|
1396
|
+
agent,
|
|
1397
|
+
task.title,
|
|
1398
|
+
agentThreadSessionId || undefined,
|
|
1399
|
+
taskCwd,
|
|
1400
|
+
taskRoutePreferences,
|
|
1401
|
+
)
|
|
1155
1402
|
}
|
|
1156
1403
|
if (linkedSchedule && linkedSchedule.lastSessionId !== sessionId) {
|
|
1157
1404
|
linkedSchedule.lastSessionId = sessionId
|
|
@@ -1160,9 +1407,22 @@ export async function processNext() {
|
|
|
1160
1407
|
saveSchedules(schedules)
|
|
1161
1408
|
}
|
|
1162
1409
|
} else {
|
|
1163
|
-
sessionId =
|
|
1410
|
+
sessionId = reusableTaskSessionId || createOrchestratorSession(
|
|
1411
|
+
agent,
|
|
1412
|
+
task.title,
|
|
1413
|
+
agentThreadSessionId || undefined,
|
|
1414
|
+
taskCwd,
|
|
1415
|
+
taskRoutePreferences,
|
|
1416
|
+
)
|
|
1164
1417
|
}
|
|
1165
1418
|
|
|
1419
|
+
const executionSessions = loadSessions() as Record<string, Session>
|
|
1420
|
+
const executionSession = executionSessions[sessionId]
|
|
1421
|
+
const seededResumeState = executionSession
|
|
1422
|
+
? applyTaskResumeStateToSession(executionSession, resumeContext?.resume)
|
|
1423
|
+
: false
|
|
1424
|
+
if (seededResumeState) saveSessions(executionSessions)
|
|
1425
|
+
|
|
1166
1426
|
// Notify the agent's thread that a task has started
|
|
1167
1427
|
if (agentThreadSessionId) {
|
|
1168
1428
|
try {
|
|
@@ -1188,9 +1448,19 @@ export async function processNext() {
|
|
|
1188
1448
|
}
|
|
1189
1449
|
|
|
1190
1450
|
task.sessionId = sessionId
|
|
1451
|
+
const reusedExistingSession = !isScheduleTask && Boolean(reusableTaskSessionId) && reusableTaskSessionId === sessionId
|
|
1452
|
+
const continuationBits: string[] = []
|
|
1453
|
+
if (reusedExistingSession) {
|
|
1454
|
+
continuationBits.push('reusing prior session')
|
|
1455
|
+
}
|
|
1456
|
+
if (resumeContext?.source === 'delegated_from_task' || resumeContext?.source === 'blocked_by') {
|
|
1457
|
+
continuationBits.push(`seeded from task ${resumeContext.sourceTaskId}`)
|
|
1458
|
+
} else if (seededResumeState) {
|
|
1459
|
+
continuationBits.push('restored CLI resume handles')
|
|
1460
|
+
}
|
|
1191
1461
|
task.checkpoint = {
|
|
1192
1462
|
lastSessionId: sessionId,
|
|
1193
|
-
note: `Attempt ${(task.attempts || 0) + 1}/${task.maxAttempts || '?'} started`,
|
|
1463
|
+
note: `Attempt ${(task.attempts || 0) + 1}/${task.maxAttempts || '?'} started${continuationBits.length ? ` (${continuationBits.join('; ')})` : ''}`,
|
|
1194
1464
|
updatedAt: Date.now(),
|
|
1195
1465
|
}
|
|
1196
1466
|
saveTasks(tasks)
|
|
@@ -1208,9 +1478,9 @@ export async function processNext() {
|
|
|
1208
1478
|
const delegatorId = (task as unknown as Record<string, unknown>).delegatedByAgentId as string | undefined
|
|
1209
1479
|
const delegator = delegatorId ? agents[delegatorId] : null
|
|
1210
1480
|
const prefix = `[delegation-source:${delegatorId || ''}:${delegator?.name || 'Agent'}:${delegator?.avatarSeed || ''}]`
|
|
1211
|
-
initialText = `${prefix}\nDelegated by **${delegator?.name || 'another agent'}** | [${task.title}](#task:${task.id})\n\n${task.description || ''}\n\nWorking directory: \`${taskCwd}
|
|
1481
|
+
initialText = `${prefix}\nDelegated by **${delegator?.name || 'another agent'}** | [${task.title}](#task:${task.id})\n\n${task.description || ''}\n\nWorking directory: \`${taskCwd}\`${buildTaskContinuationNote(Boolean(reusedExistingSession), resumeContext)}\n\nI'll begin working on this now.`
|
|
1212
1482
|
} else {
|
|
1213
|
-
initialText = `Starting task: **${task.title}**\n\n${task.description || ''}\n\nWorking directory: \`${taskCwd}
|
|
1483
|
+
initialText = `Starting task: **${task.title}**\n\n${task.description || ''}\n\nWorking directory: \`${taskCwd}\`${buildTaskContinuationNote(Boolean(reusedExistingSession), resumeContext)}\n\nI'll begin working on this now.`
|
|
1214
1484
|
}
|
|
1215
1485
|
sessions[sessionId].messages.push({
|
|
1216
1486
|
role: 'assistant',
|
|
@@ -7,6 +7,7 @@ import { log } from './logger'
|
|
|
7
7
|
import { isInternalHeartbeatRun } from './heartbeat-source'
|
|
8
8
|
import { cleanupSessionBrowser } from './session-tools/web'
|
|
9
9
|
import { cancelDelegationJobsForParentSession } from './delegation-jobs'
|
|
10
|
+
import { handleMainLoopRunResult } from './main-agent-loop'
|
|
10
11
|
|
|
11
12
|
export type SessionRunStatus = 'queued' | 'running' | 'completed' | 'failed' | 'cancelled'
|
|
12
13
|
export type SessionQueueMode = 'followup' | 'steer' | 'collect'
|
|
@@ -276,6 +277,36 @@ async function drainExecution(executionKey: string): Promise<void> {
|
|
|
276
277
|
error: next.run.error || null,
|
|
277
278
|
durationMs: (next.run.endedAt || now()) - (next.run.startedAt || now()),
|
|
278
279
|
})
|
|
280
|
+
const followup = handleMainLoopRunResult({
|
|
281
|
+
sessionId: next.run.sessionId,
|
|
282
|
+
message: next.message,
|
|
283
|
+
internal: next.run.internal,
|
|
284
|
+
source: next.run.source,
|
|
285
|
+
resultText: result.text,
|
|
286
|
+
error: next.run.error,
|
|
287
|
+
toolEvents: result.toolEvents,
|
|
288
|
+
inputTokens: result.inputTokens,
|
|
289
|
+
outputTokens: result.outputTokens,
|
|
290
|
+
estimatedCost: result.estimatedCost,
|
|
291
|
+
})
|
|
292
|
+
if (followup) {
|
|
293
|
+
setTimeout(() => {
|
|
294
|
+
try {
|
|
295
|
+
enqueueSessionRun({
|
|
296
|
+
sessionId: next.run.sessionId,
|
|
297
|
+
message: followup.message,
|
|
298
|
+
internal: true,
|
|
299
|
+
source: 'main-loop-followup',
|
|
300
|
+
mode: 'followup',
|
|
301
|
+
dedupeKey: followup.dedupeKey,
|
|
302
|
+
})
|
|
303
|
+
} catch (err: unknown) {
|
|
304
|
+
log.warn('session-run', `Main loop follow-up enqueue failed for ${next.run.sessionId}`, {
|
|
305
|
+
error: err instanceof Error ? err.message : String(err),
|
|
306
|
+
})
|
|
307
|
+
}
|
|
308
|
+
}, Math.max(0, followup.delayMs || 0))
|
|
309
|
+
}
|
|
279
310
|
next.resolve(result)
|
|
280
311
|
} catch (err: any) {
|
|
281
312
|
const aborted = next.signalController.signal.aborted
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { describe, it } from 'node:test'
|
|
5
|
+
import { UPLOAD_DIR } from '../storage'
|
|
6
|
+
import { resolveConnectorMediaInput } from './connector'
|
|
7
|
+
|
|
8
|
+
describe('resolveConnectorMediaInput', () => {
|
|
9
|
+
it('resolves /api/uploads urls passed via mediaPath back to disk', () => {
|
|
10
|
+
const filename = `screenshot-test-${Date.now()}.png`
|
|
11
|
+
const uploadPath = path.join(UPLOAD_DIR, filename)
|
|
12
|
+
fs.mkdirSync(UPLOAD_DIR, { recursive: true })
|
|
13
|
+
fs.writeFileSync(uploadPath, 'png')
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const resolved = resolveConnectorMediaInput({
|
|
17
|
+
cwd: process.cwd(),
|
|
18
|
+
mediaPath: `/api/uploads/${filename}`,
|
|
19
|
+
})
|
|
20
|
+
assert.equal(resolved.error, undefined)
|
|
21
|
+
assert.equal(resolved.mediaPath, uploadPath)
|
|
22
|
+
} finally {
|
|
23
|
+
fs.rmSync(uploadPath, { force: true })
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('treats remote urls passed via mediaPath as sendable urls instead of local files', () => {
|
|
28
|
+
const resolved = resolveConnectorMediaInput({
|
|
29
|
+
cwd: process.cwd(),
|
|
30
|
+
mediaPath: 'https://example.com/report.pdf',
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
assert.equal(resolved.error, undefined)
|
|
34
|
+
assert.equal(resolved.mediaPath, undefined)
|
|
35
|
+
assert.equal(resolved.fileUrl, 'https://example.com/report.pdf')
|
|
36
|
+
})
|
|
37
|
+
})
|
|
@@ -189,7 +189,7 @@ function pickChannelTarget(params: {
|
|
|
189
189
|
return { channelId }
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
function resolveConnectorMediaInput(params: {
|
|
192
|
+
export function resolveConnectorMediaInput(params: {
|
|
193
193
|
cwd: string
|
|
194
194
|
mediaPath?: string
|
|
195
195
|
imageUrl?: string
|
|
@@ -199,6 +199,23 @@ function resolveConnectorMediaInput(params: {
|
|
|
199
199
|
let resolvedImageUrl = params.imageUrl?.trim() || undefined
|
|
200
200
|
let resolvedFileUrl = params.fileUrl?.trim() || undefined
|
|
201
201
|
|
|
202
|
+
// Be forgiving when the model passes a served upload URL or remote URL in mediaPath.
|
|
203
|
+
if (resolvedMediaPath?.startsWith('/api/uploads/')) {
|
|
204
|
+
const fromUpload = resolveUploadUrl(resolvedMediaPath)
|
|
205
|
+
if (fromUpload) {
|
|
206
|
+
resolvedMediaPath = fromUpload.mediaPath
|
|
207
|
+
} else {
|
|
208
|
+
return { error: `Error: File not found: ${resolvedMediaPath}` }
|
|
209
|
+
}
|
|
210
|
+
} else if (resolvedMediaPath && /^https?:\/\//i.test(resolvedMediaPath)) {
|
|
211
|
+
if (/\.(png|jpe?g|webp|gif|svg)(?:[?#].*)?$/i.test(resolvedMediaPath)) {
|
|
212
|
+
resolvedImageUrl = resolvedMediaPath
|
|
213
|
+
} else {
|
|
214
|
+
resolvedFileUrl = resolvedMediaPath
|
|
215
|
+
}
|
|
216
|
+
resolvedMediaPath = undefined
|
|
217
|
+
}
|
|
218
|
+
|
|
202
219
|
if (resolvedMediaPath && !path.isAbsolute(resolvedMediaPath) && !resolvedMediaPath.startsWith('/api/uploads/')) {
|
|
203
220
|
const candidatePaths = [
|
|
204
221
|
path.resolve(params.cwd, resolvedMediaPath),
|
|
@@ -540,6 +557,14 @@ const ConnectorPlugin: Plugin = {
|
|
|
540
557
|
messageId: { type: 'string' },
|
|
541
558
|
targetMessage: { type: 'string', enum: ['last_inbound', 'last_outbound'] },
|
|
542
559
|
emoji: { type: 'string' },
|
|
560
|
+
voiceText: { type: 'string' },
|
|
561
|
+
voiceId: { type: 'string' },
|
|
562
|
+
imageUrl: { type: 'string' },
|
|
563
|
+
fileUrl: { type: 'string' },
|
|
564
|
+
mediaPath: { type: 'string' },
|
|
565
|
+
mimeType: { type: 'string' },
|
|
566
|
+
fileName: { type: 'string' },
|
|
567
|
+
caption: { type: 'string' },
|
|
543
568
|
replyToMessageId: { type: 'string' },
|
|
544
569
|
threadId: { type: 'string' },
|
|
545
570
|
delaySec: { type: 'number' },
|
|
@@ -9,6 +9,11 @@ export interface ToolContext {
|
|
|
9
9
|
platformAssignScope?: 'self' | 'all'
|
|
10
10
|
mcpServerIds?: string[]
|
|
11
11
|
mcpDisabledTools?: string[]
|
|
12
|
+
projectId?: string | null
|
|
13
|
+
projectRoot?: string | null
|
|
14
|
+
projectName?: string | null
|
|
15
|
+
projectDescription?: string | null
|
|
16
|
+
memoryScopeMode?: 'auto' | 'all' | 'global' | 'agent' | 'session' | 'project' | null
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
export interface SessionToolsResult {
|