@swarmclawai/swarmclaw 0.7.7 → 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 +10 -9
- package/package.json +1 -1
- package/src/app/api/chats/route.ts +1 -0
- package/src/app/api/connectors/[id]/route.ts +20 -2
- package/src/app/api/connectors/route.ts +12 -8
- 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/components/agents/agent-sheet.tsx +184 -14
- 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/layout/app-layout.tsx +40 -23
- package/src/components/projects/project-detail.tsx +217 -0
- package/src/components/projects/project-sheet.tsx +176 -4
- 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/capability-router.test.ts +22 -0
- package/src/lib/server/capability-router.ts +54 -18
- package/src/lib/server/chat-execution.ts +25 -1
- 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/orchestrator-lg.ts +1 -0
- package/src/lib/server/orchestrator.ts +2 -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 +234 -7
- 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/tool-definitions.ts +2 -1
- package/src/types/index.ts +39 -0
|
@@ -14,11 +14,23 @@ import { toast } from 'sonner'
|
|
|
14
14
|
const ACTIVE_COLUMNS: BoardTaskStatus[] = ['backlog', 'queued', 'running', 'completed', 'failed']
|
|
15
15
|
type BoardViewMode = 'board' | 'list'
|
|
16
16
|
type AttentionFilter = 'all' | 'needs-attention' | 'approval' | 'blocked' | 'overdue' | 'failed'
|
|
17
|
+
type TaskScopeFilter = 'user-facing' | 'all' | 'agent'
|
|
17
18
|
|
|
18
19
|
function isTaskOverdue(task: BoardTask): boolean {
|
|
19
20
|
return !!task.dueAt && task.dueAt < Date.now() && task.status !== 'completed' && task.status !== 'archived'
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
function isInternalAgentTask(task: BoardTask): boolean {
|
|
24
|
+
if (task.sourceType === 'schedule' || task.sourceType === 'delegation') return true
|
|
25
|
+
return Boolean(task.createdByAgentId || task.delegatedByAgentId)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isTaskRelevantToAgent(task: BoardTask, agentId: string): boolean {
|
|
29
|
+
return task.agentId === agentId
|
|
30
|
+
|| task.createdByAgentId === agentId
|
|
31
|
+
|| task.delegatedByAgentId === agentId
|
|
32
|
+
}
|
|
33
|
+
|
|
22
34
|
function matchesAttentionFilter(task: BoardTask, filter: AttentionFilter): boolean {
|
|
23
35
|
const blocked = !!task.blockedBy?.length
|
|
24
36
|
const pendingApproval = !!task.pendingApproval
|
|
@@ -139,6 +151,13 @@ export function TaskBoard() {
|
|
|
139
151
|
if (typeof window === 'undefined') return ''
|
|
140
152
|
return new URLSearchParams(window.location.search).get('tag') || ''
|
|
141
153
|
})
|
|
154
|
+
const [taskScopeFilter, setTaskScopeFilter] = useState<TaskScopeFilter>(() => {
|
|
155
|
+
if (typeof window === 'undefined') return 'user-facing'
|
|
156
|
+
const params = new URLSearchParams(window.location.search)
|
|
157
|
+
if (params.get('agent')) return 'agent'
|
|
158
|
+
const raw = params.get('taskView')
|
|
159
|
+
return raw === 'all' ? 'all' : 'user-facing'
|
|
160
|
+
})
|
|
142
161
|
const [viewMode, setViewMode] = useState<BoardViewMode>('board')
|
|
143
162
|
const [attentionFilter, setAttentionFilter] = useState<AttentionFilter>('all')
|
|
144
163
|
|
|
@@ -156,13 +175,14 @@ export function TaskBoard() {
|
|
|
156
175
|
useEffect(() => {
|
|
157
176
|
if (typeof window === 'undefined') return
|
|
158
177
|
const params = new URLSearchParams()
|
|
159
|
-
if (filterAgentId) params.set('agent', filterAgentId)
|
|
178
|
+
if (taskScopeFilter === 'agent' && filterAgentId) params.set('agent', filterAgentId)
|
|
179
|
+
else if (taskScopeFilter === 'all') params.set('taskView', 'all')
|
|
160
180
|
if (filterTag) params.set('tag', filterTag)
|
|
161
181
|
if (activeProjectFilter) params.set('project', activeProjectFilter)
|
|
162
182
|
const qs = params.toString()
|
|
163
183
|
const newUrl = `${window.location.pathname}${qs ? `?${qs}` : ''}`
|
|
164
184
|
window.history.replaceState(null, '', newUrl)
|
|
165
|
-
}, [filterAgentId, filterTag, activeProjectFilter])
|
|
185
|
+
}, [filterAgentId, filterTag, activeProjectFilter, taskScopeFilter])
|
|
166
186
|
|
|
167
187
|
const [loaded, setLoaded] = useState(Object.keys(tasks).length > 0)
|
|
168
188
|
useEffect(() => { Promise.all([loadTasks(), loadAgents(), loadProjects()]).then(() => setLoaded(true)) }, [])
|
|
@@ -173,17 +193,28 @@ export function TaskBoard() {
|
|
|
173
193
|
|
|
174
194
|
const columns: BoardTaskStatus[] = showArchived ? [...ACTIVE_COLUMNS, 'archived'] : ACTIVE_COLUMNS
|
|
175
195
|
|
|
176
|
-
const
|
|
196
|
+
const matchesScopeFilters = useCallback((task: BoardTask) => {
|
|
177
197
|
if (!showArchived && task.status === 'archived') return false
|
|
178
|
-
if (
|
|
198
|
+
if (taskScopeFilter === 'user-facing' && isInternalAgentTask(task)) return false
|
|
199
|
+
if (taskScopeFilter === 'agent' && (!filterAgentId || !isTaskRelevantToAgent(task, filterAgentId))) return false
|
|
179
200
|
if (filterTag && !(task.tags && task.tags.includes(filterTag))) return false
|
|
180
201
|
if (activeProjectFilter && task.projectId !== activeProjectFilter) return false
|
|
202
|
+
return true
|
|
203
|
+
}, [activeProjectFilter, filterAgentId, filterTag, showArchived, taskScopeFilter])
|
|
204
|
+
|
|
205
|
+
const matchesBaseFilters = useCallback((task: BoardTask) => {
|
|
206
|
+
if (!matchesScopeFilters(task)) return false
|
|
181
207
|
if (!matchesAttentionFilter(task, attentionFilter)) return false
|
|
182
208
|
return true
|
|
183
|
-
}, [
|
|
209
|
+
}, [attentionFilter, matchesScopeFilters])
|
|
210
|
+
|
|
211
|
+
const scopedTasks = useMemo(
|
|
212
|
+
() => Object.values(tasks).filter(matchesScopeFilters),
|
|
213
|
+
[tasks, matchesScopeFilters],
|
|
214
|
+
)
|
|
184
215
|
|
|
185
216
|
const filteredTasks = useMemo(() => (
|
|
186
|
-
|
|
217
|
+
scopedTasks
|
|
187
218
|
.filter(matchesBaseFilters)
|
|
188
219
|
.sort((a, b) => {
|
|
189
220
|
const rankDiff = attentionRank(a) - attentionRank(b)
|
|
@@ -192,7 +223,7 @@ export function TaskBoard() {
|
|
|
192
223
|
if (dueDiff !== 0) return dueDiff
|
|
193
224
|
return b.updatedAt - a.updatedAt
|
|
194
225
|
})
|
|
195
|
-
), [
|
|
226
|
+
), [scopedTasks, matchesBaseFilters])
|
|
196
227
|
|
|
197
228
|
const tasksByStatus = useCallback((status: BoardTaskStatus) =>
|
|
198
229
|
filteredTasks
|
|
@@ -223,7 +254,7 @@ export function TaskBoard() {
|
|
|
223
254
|
|
|
224
255
|
// Task counts per project (non-archived)
|
|
225
256
|
const projectTaskCounts: Record<string, number> = {}
|
|
226
|
-
for (const t of
|
|
257
|
+
for (const t of scopedTasks) {
|
|
227
258
|
if (t.projectId && t.status !== 'archived') {
|
|
228
259
|
projectTaskCounts[t.projectId] = (projectTaskCounts[t.projectId] || 0) + 1
|
|
229
260
|
}
|
|
@@ -231,7 +262,7 @@ export function TaskBoard() {
|
|
|
231
262
|
|
|
232
263
|
// Summary stats
|
|
233
264
|
const stats = useMemo(() => {
|
|
234
|
-
const all =
|
|
265
|
+
const all = scopedTasks.filter((t) => t.status !== 'archived')
|
|
235
266
|
return {
|
|
236
267
|
total: all.length,
|
|
237
268
|
running: all.filter((t) => t.status === 'running').length,
|
|
@@ -242,7 +273,13 @@ export function TaskBoard() {
|
|
|
242
273
|
approvals: all.filter((t) => !!t.pendingApproval).length,
|
|
243
274
|
attention: all.filter((t) => matchesAttentionFilter(t, 'needs-attention')).length,
|
|
244
275
|
}
|
|
245
|
-
}, [
|
|
276
|
+
}, [scopedTasks])
|
|
277
|
+
|
|
278
|
+
const activeScopeLabel = useMemo(() => {
|
|
279
|
+
if (taskScopeFilter === 'all') return 'All tasks'
|
|
280
|
+
if (taskScopeFilter === 'agent' && filterAgentId && agents[filterAgentId]) return `${agents[filterAgentId].name} activity`
|
|
281
|
+
return 'User-facing tasks'
|
|
282
|
+
}, [agents, filterAgentId, taskScopeFilter])
|
|
246
283
|
|
|
247
284
|
const activeAttentionLabel = useMemo(() => {
|
|
248
285
|
if (attentionFilter === 'all') return null
|
|
@@ -301,6 +338,16 @@ export function TaskBoard() {
|
|
|
301
338
|
<p className="text-[13px] text-text-3">
|
|
302
339
|
{stats.total} task{stats.total !== 1 ? 's' : ''}
|
|
303
340
|
</p>
|
|
341
|
+
<span className="inline-flex items-center gap-1 rounded-full bg-white/[0.04] px-2 py-1 text-[11px] font-600 text-text-2">
|
|
342
|
+
{taskScopeFilter === 'agent' && filterAgentId && agents[filterAgentId] ? (
|
|
343
|
+
<>
|
|
344
|
+
<AgentAvatar seed={agents[filterAgentId].avatarSeed || null} avatarUrl={agents[filterAgentId].avatarUrl} name={agents[filterAgentId].name} size={14} />
|
|
345
|
+
{activeScopeLabel}
|
|
346
|
+
</>
|
|
347
|
+
) : (
|
|
348
|
+
activeScopeLabel
|
|
349
|
+
)}
|
|
350
|
+
</span>
|
|
304
351
|
{stats.running > 0 && (
|
|
305
352
|
<span className="inline-flex items-center gap-1 text-[11px] font-600 text-blue-400">
|
|
306
353
|
<span className="w-1.5 h-1.5 rounded-full bg-blue-400 animate-pulse" />
|
|
@@ -338,41 +385,83 @@ export function TaskBoard() {
|
|
|
338
385
|
<button
|
|
339
386
|
onClick={() => setAgentDropdownOpen(!agentDropdownOpen)}
|
|
340
387
|
className={`flex items-center gap-2 px-3 py-2 rounded-[10px] text-[13px] font-600 cursor-pointer transition-all border
|
|
341
|
-
${
|
|
388
|
+
${taskScopeFilter !== 'user-facing'
|
|
342
389
|
? 'bg-white/[0.06] border-white/[0.1] text-text-2'
|
|
343
390
|
: 'bg-transparent border-white/[0.06] text-text-3 hover:bg-white/[0.03]'}`}
|
|
344
391
|
style={{ fontFamily: 'inherit', minWidth: 130 }}
|
|
345
392
|
>
|
|
346
|
-
{filterAgentId && agents[filterAgentId] ? (
|
|
393
|
+
{taskScopeFilter === 'agent' && filterAgentId && agents[filterAgentId] ? (
|
|
347
394
|
<>
|
|
348
395
|
<AgentAvatar seed={agents[filterAgentId].avatarSeed || null} avatarUrl={agents[filterAgentId].avatarUrl} name={agents[filterAgentId].name} size={18} />
|
|
349
396
|
{agents[filterAgentId].name}
|
|
350
397
|
</>
|
|
351
|
-
) : 'All
|
|
398
|
+
) : taskScopeFilter === 'all' ? 'All Tasks' : 'User View'}
|
|
352
399
|
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" className="ml-auto opacity-50">
|
|
353
400
|
<path d="M2.5 4L5 6.5L7.5 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
354
401
|
</svg>
|
|
355
402
|
</button>
|
|
356
403
|
{agentDropdownOpen && (
|
|
357
|
-
<div className="absolute top-full right-0 mt-1 min-w-[
|
|
404
|
+
<div className="absolute top-full right-0 mt-1 min-w-[240px] py-1 rounded-[12px] border border-white/[0.08] bg-surface-2 shadow-lg z-50">
|
|
405
|
+
<button
|
|
406
|
+
onClick={() => {
|
|
407
|
+
setTaskScopeFilter('user-facing')
|
|
408
|
+
setFilterAgentId('')
|
|
409
|
+
setAgentDropdownOpen(false)
|
|
410
|
+
}}
|
|
411
|
+
className={`w-full flex items-start gap-2.5 px-3 py-2.5 text-[13px] font-600 cursor-pointer border-none text-left transition-colors
|
|
412
|
+
${taskScopeFilter === 'user-facing' ? 'bg-white/[0.06] text-text' : 'bg-transparent text-text-3 hover:bg-white/[0.04]'}`}
|
|
413
|
+
style={{ fontFamily: 'inherit' }}
|
|
414
|
+
>
|
|
415
|
+
<span className="mt-0.5 inline-flex h-5 items-center rounded-full bg-emerald-500/12 px-1.5 text-[10px] font-700 uppercase tracking-[0.08em] text-emerald-400">
|
|
416
|
+
Default
|
|
417
|
+
</span>
|
|
418
|
+
<span className="min-w-0">
|
|
419
|
+
<span className="block">User-facing tasks</span>
|
|
420
|
+
<span className="mt-0.5 block text-[11px] font-500 text-text-3/60">
|
|
421
|
+
Hide scheduled, delegated, and agent-created internal work.
|
|
422
|
+
</span>
|
|
423
|
+
</span>
|
|
424
|
+
</button>
|
|
358
425
|
<button
|
|
359
|
-
onClick={() => {
|
|
360
|
-
|
|
361
|
-
|
|
426
|
+
onClick={() => {
|
|
427
|
+
setTaskScopeFilter('all')
|
|
428
|
+
setFilterAgentId('')
|
|
429
|
+
setAgentDropdownOpen(false)
|
|
430
|
+
}}
|
|
431
|
+
className={`w-full flex items-start gap-2.5 px-3 py-2.5 text-[13px] font-600 cursor-pointer border-none text-left transition-colors
|
|
432
|
+
${taskScopeFilter === 'all' ? 'bg-white/[0.06] text-text' : 'bg-transparent text-text-3 hover:bg-white/[0.04]'}`}
|
|
362
433
|
style={{ fontFamily: 'inherit' }}
|
|
363
434
|
>
|
|
364
|
-
|
|
435
|
+
<span className="mt-0.5 inline-flex h-5 items-center rounded-full bg-white/[0.06] px-1.5 text-[10px] font-700 uppercase tracking-[0.08em] text-text-3">
|
|
436
|
+
All
|
|
437
|
+
</span>
|
|
438
|
+
<span className="min-w-0">
|
|
439
|
+
<span className="block">All tasks</span>
|
|
440
|
+
<span className="mt-0.5 block text-[11px] font-500 text-text-3/60">
|
|
441
|
+
Include internal agent execution, schedules, and delegations.
|
|
442
|
+
</span>
|
|
443
|
+
</span>
|
|
365
444
|
</button>
|
|
445
|
+
<div className="my-1 border-t border-white/[0.06]" />
|
|
366
446
|
{Object.values(agents).sort((a, b) => a.name.localeCompare(b.name)).map((a) => (
|
|
367
447
|
<button
|
|
368
448
|
key={a.id}
|
|
369
|
-
onClick={() => {
|
|
449
|
+
onClick={() => {
|
|
450
|
+
setTaskScopeFilter('agent')
|
|
451
|
+
setFilterAgentId(a.id)
|
|
452
|
+
setAgentDropdownOpen(false)
|
|
453
|
+
}}
|
|
370
454
|
className={`w-full flex items-center gap-2.5 px-3 py-2 text-[13px] font-600 cursor-pointer border-none text-left transition-colors
|
|
371
|
-
${filterAgentId === a.id ? 'bg-white/[0.06] text-text' : 'bg-transparent text-text-3 hover:bg-white/[0.04]'}`}
|
|
455
|
+
${taskScopeFilter === 'agent' && filterAgentId === a.id ? 'bg-white/[0.06] text-text' : 'bg-transparent text-text-3 hover:bg-white/[0.04]'}`}
|
|
372
456
|
style={{ fontFamily: 'inherit' }}
|
|
373
457
|
>
|
|
374
458
|
<AgentAvatar seed={a.avatarSeed || null} avatarUrl={a.avatarUrl} name={a.name} size={20} />
|
|
375
|
-
|
|
459
|
+
<span className="min-w-0 flex-1">
|
|
460
|
+
<span className="block truncate">{a.name}</span>
|
|
461
|
+
<span className="mt-0.5 block text-[11px] font-500 text-text-3/60">
|
|
462
|
+
Assigned, created, or delegated by this agent
|
|
463
|
+
</span>
|
|
464
|
+
</span>
|
|
376
465
|
</button>
|
|
377
466
|
))}
|
|
378
467
|
</div>
|
|
@@ -520,8 +609,33 @@ export function TaskBoard() {
|
|
|
520
609
|
))}
|
|
521
610
|
</div>
|
|
522
611
|
|
|
523
|
-
{(activeProjectFilter && projects[activeProjectFilter]) || activeAttentionLabel ? (
|
|
612
|
+
{(activeProjectFilter && projects[activeProjectFilter]) || activeAttentionLabel || taskScopeFilter !== 'all' ? (
|
|
524
613
|
<div className="flex flex-wrap items-center gap-2 px-8 pb-3">
|
|
614
|
+
{taskScopeFilter !== 'all' && (
|
|
615
|
+
<span className={`inline-flex items-center gap-1.5 px-3 py-1.5 rounded-[8px] border text-[12px] font-600 ${
|
|
616
|
+
taskScopeFilter === 'agent'
|
|
617
|
+
? 'bg-accent-soft border-accent-bright/20 text-accent-bright'
|
|
618
|
+
: 'bg-emerald-500/10 border-emerald-500/20 text-emerald-400'
|
|
619
|
+
}`}>
|
|
620
|
+
{taskScopeFilter === 'agent' && filterAgentId && agents[filterAgentId] ? (
|
|
621
|
+
<>
|
|
622
|
+
<AgentAvatar seed={agents[filterAgentId].avatarSeed || null} avatarUrl={agents[filterAgentId].avatarUrl} name={agents[filterAgentId].name} size={14} />
|
|
623
|
+
{agents[filterAgentId].name} activity
|
|
624
|
+
</>
|
|
625
|
+
) : (
|
|
626
|
+
'User-facing tasks'
|
|
627
|
+
)}
|
|
628
|
+
<button
|
|
629
|
+
onClick={() => {
|
|
630
|
+
setTaskScopeFilter('all')
|
|
631
|
+
setFilterAgentId('')
|
|
632
|
+
}}
|
|
633
|
+
className="ml-1 cursor-pointer border-none bg-transparent p-0 text-[14px] leading-none text-current opacity-80 hover:opacity-100"
|
|
634
|
+
>
|
|
635
|
+
×
|
|
636
|
+
</button>
|
|
637
|
+
</span>
|
|
638
|
+
)}
|
|
525
639
|
{activeProjectFilter && projects[activeProjectFilter] && (
|
|
526
640
|
<span className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-[8px] bg-white/[0.04] border border-white/[0.06] text-[12px] font-600 text-text-2">
|
|
527
641
|
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: projects[activeProjectFilter].color || '#6366F1' }} />
|
|
@@ -593,7 +707,7 @@ export function TaskBoard() {
|
|
|
593
707
|
) : filteredTasks.length === 0 ? (
|
|
594
708
|
<div className="max-w-3xl mx-auto rounded-[16px] border border-dashed border-white/[0.08] px-6 py-14 text-center">
|
|
595
709
|
<p className="text-[14px] font-600 text-text-2 mb-1">No tasks match this view</p>
|
|
596
|
-
<p className="text-[12px] text-text-3/60">Try clearing one of the active filters or switching back to
|
|
710
|
+
<p className="text-[12px] text-text-3/60">Try clearing one of the active filters or switching back to all tasks.</p>
|
|
597
711
|
</div>
|
|
598
712
|
) : (
|
|
599
713
|
<div className="max-w-4xl mx-auto">
|
|
@@ -5,6 +5,7 @@ import { useAppStore } from '@/stores/use-app-store'
|
|
|
5
5
|
import { api } from '@/lib/api-client'
|
|
6
6
|
import { updateTask, archiveTask } from '@/lib/tasks'
|
|
7
7
|
import { ConfirmDialog } from '@/components/shared/confirm-dialog'
|
|
8
|
+
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
8
9
|
import type { BoardTask } from '@/types'
|
|
9
10
|
|
|
10
11
|
function timeAgo(ts: number) {
|
|
@@ -46,6 +47,8 @@ export function TaskCard({ task, selectionMode, selected, onToggleSelect, index
|
|
|
46
47
|
const tasks = useAppStore((s) => s.tasks)
|
|
47
48
|
const agent = agents[task.agentId]
|
|
48
49
|
const project = task.projectId ? projects[task.projectId] : null
|
|
50
|
+
const creatorAgent = task.createdByAgentId ? agents[task.createdByAgentId] : null
|
|
51
|
+
const delegatorAgent = task.delegatedByAgentId ? agents[task.delegatedByAgentId] : null
|
|
49
52
|
|
|
50
53
|
const priorityConfig = {
|
|
51
54
|
critical: { label: 'Critical', cls: 'bg-red-500/10 text-red-400' },
|
|
@@ -207,6 +210,32 @@ export function TaskCard({ task, selectionMode, selected, onToggleSelect, index
|
|
|
207
210
|
</div>
|
|
208
211
|
)}
|
|
209
212
|
|
|
213
|
+
{(creatorAgent || delegatorAgent || task.sourceType === 'schedule') && (
|
|
214
|
+
<div className="flex flex-wrap gap-1.5 mb-3">
|
|
215
|
+
{delegatorAgent && (
|
|
216
|
+
<span className="inline-flex items-center gap-1.5 rounded-[7px] bg-amber-500/10 px-2 py-1 text-[10px] font-600 text-amber-300">
|
|
217
|
+
<AgentAvatar seed={delegatorAgent.avatarSeed} avatarUrl={delegatorAgent.avatarUrl} name={delegatorAgent.name} size={14} />
|
|
218
|
+
Delegated by {delegatorAgent.name}
|
|
219
|
+
</span>
|
|
220
|
+
)}
|
|
221
|
+
{creatorAgent && creatorAgent.id !== delegatorAgent?.id && (
|
|
222
|
+
<span className="inline-flex items-center gap-1.5 rounded-[7px] bg-white/[0.05] px-2 py-1 text-[10px] font-600 text-text-2">
|
|
223
|
+
<AgentAvatar seed={creatorAgent.avatarSeed} avatarUrl={creatorAgent.avatarUrl} name={creatorAgent.name} size={14} />
|
|
224
|
+
Created by {creatorAgent.name}
|
|
225
|
+
</span>
|
|
226
|
+
)}
|
|
227
|
+
{task.sourceType === 'schedule' && (
|
|
228
|
+
<span className="inline-flex items-center gap-1.5 rounded-[7px] bg-purple-500/10 px-2 py-1 text-[10px] font-600 text-purple-300">
|
|
229
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
230
|
+
<circle cx="12" cy="12" r="8" />
|
|
231
|
+
<path d="M12 8v4l3 2" />
|
|
232
|
+
</svg>
|
|
233
|
+
{task.sourceScheduleName ? `Scheduled via ${task.sourceScheduleName}` : 'Scheduled task'}
|
|
234
|
+
</span>
|
|
235
|
+
)}
|
|
236
|
+
</div>
|
|
237
|
+
)}
|
|
238
|
+
|
|
210
239
|
<div className="flex items-center gap-2 flex-wrap">
|
|
211
240
|
{agent && (
|
|
212
241
|
<span className="px-2 py-1 rounded-[6px] bg-accent-soft text-accent-bright text-[11px] font-600">
|
|
@@ -467,7 +467,7 @@ export function TaskSheet() {
|
|
|
467
467
|
)}
|
|
468
468
|
|
|
469
469
|
{/* CLI Sessions */}
|
|
470
|
-
{(editing.claudeResumeId || editing.codexResumeId || editing.opencodeResumeId || editing.cliResumeId) && (
|
|
470
|
+
{(editing.claudeResumeId || editing.codexResumeId || editing.opencodeResumeId || editing.geminiResumeId || editing.cliResumeId) && (
|
|
471
471
|
<div className="mb-8">
|
|
472
472
|
<SectionLabel>CLI Sessions</SectionLabel>
|
|
473
473
|
<div className="flex flex-wrap gap-2">
|
|
@@ -489,7 +489,13 @@ export function TaskSheet() {
|
|
|
489
489
|
<code className="text-[11px] text-text-3 font-mono">{editing.opencodeResumeId}</code>
|
|
490
490
|
</div>
|
|
491
491
|
)}
|
|
492
|
-
{
|
|
492
|
+
{editing.geminiResumeId && (
|
|
493
|
+
<div className="flex items-center gap-2 px-3 py-2 rounded-[10px] border border-white/[0.06] bg-surface">
|
|
494
|
+
<span className="text-[11px] font-600 text-fuchsia-400">Gemini</span>
|
|
495
|
+
<code className="text-[11px] text-text-3 font-mono">{editing.geminiResumeId}</code>
|
|
496
|
+
</div>
|
|
497
|
+
)}
|
|
498
|
+
{!(editing.claudeResumeId || editing.codexResumeId || editing.opencodeResumeId || editing.geminiResumeId) && editing.cliResumeId && (
|
|
493
499
|
<div className="flex items-center gap-2 px-3 py-2 rounded-[10px] border border-white/[0.06] bg-surface">
|
|
494
500
|
<span className="text-[11px] font-600 text-text-2">{editing.cliProvider || 'CLI'}</span>
|
|
495
501
|
<code className="text-[11px] text-text-3 font-mono">{editing.cliResumeId}</code>
|
|
@@ -971,7 +977,7 @@ export function TaskSheet() {
|
|
|
971
977
|
</div>
|
|
972
978
|
)}
|
|
973
979
|
|
|
974
|
-
{editing && (editing.claudeResumeId || editing.codexResumeId || editing.opencodeResumeId || editing.cliResumeId) && (
|
|
980
|
+
{editing && (editing.claudeResumeId || editing.codexResumeId || editing.opencodeResumeId || editing.geminiResumeId || editing.cliResumeId) && (
|
|
975
981
|
<div className="mb-8">
|
|
976
982
|
<SectionLabel>CLI Sessions</SectionLabel>
|
|
977
983
|
<div className="flex flex-wrap gap-2">
|
|
@@ -993,7 +999,13 @@ export function TaskSheet() {
|
|
|
993
999
|
<code className="text-[11px] text-text-3 font-mono">{editing.opencodeResumeId}</code>
|
|
994
1000
|
</div>
|
|
995
1001
|
)}
|
|
996
|
-
{
|
|
1002
|
+
{editing.geminiResumeId && (
|
|
1003
|
+
<div className="flex items-center gap-2 px-3 py-2 rounded-[10px] border border-white/[0.06] bg-surface">
|
|
1004
|
+
<span className="text-[11px] font-600 text-fuchsia-400">Gemini</span>
|
|
1005
|
+
<code className="text-[11px] text-text-3 font-mono">{editing.geminiResumeId}</code>
|
|
1006
|
+
</div>
|
|
1007
|
+
)}
|
|
1008
|
+
{!(editing.claudeResumeId || editing.codexResumeId || editing.opencodeResumeId || editing.geminiResumeId) && editing.cliResumeId && (
|
|
997
1009
|
<div className="flex items-center gap-2 px-3 py-2 rounded-[10px] border border-white/[0.06] bg-surface">
|
|
998
1010
|
<span className="text-[11px] font-600 text-text-2">{editing.cliProvider || 'CLI'}</span>
|
|
999
1011
|
<code className="text-[11px] text-text-3 font-mono">{editing.cliResumeId}</code>
|
|
@@ -19,3 +19,25 @@ test('routeTaskIntent keeps coding prompts prioritized over memory keywords', ()
|
|
|
19
19
|
)
|
|
20
20
|
assert.equal(decision.intent, 'coding')
|
|
21
21
|
})
|
|
22
|
+
|
|
23
|
+
test('routeTaskIntent keeps hybrid research-plus-media prompts in research intent', () => {
|
|
24
|
+
const decision = routeTaskIntent(
|
|
25
|
+
'Can you tell me more if there is any news related to the US-Iran war, and can you send me some screenshots and give me a summary and maybe send me a voice note about it?',
|
|
26
|
+
['web_search', 'web_fetch', 'browser', 'manage_connectors'],
|
|
27
|
+
null,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
assert.equal(decision.intent, 'research')
|
|
31
|
+
assert.deepEqual(decision.preferredTools, ['web_search', 'web_fetch', 'browser', 'connector_message_tool'])
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('routeTaskIntent treats direct voice-note delivery as outreach', () => {
|
|
35
|
+
const decision = routeTaskIntent(
|
|
36
|
+
'Send me a voice note over WhatsApp summarizing what changed.',
|
|
37
|
+
['manage_connectors'],
|
|
38
|
+
null,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
assert.equal(decision.intent, 'outreach')
|
|
42
|
+
assert.deepEqual(decision.preferredTools, ['connector_message_tool'])
|
|
43
|
+
})
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AppSettings } from '@/types'
|
|
2
|
+
import { getToolsForCapability, matchToolCapabilitiesForMessage, TOOL_CAPABILITY } from './tool-planning'
|
|
2
3
|
|
|
3
4
|
export type TaskIntent =
|
|
4
5
|
| 'coding'
|
|
@@ -27,6 +28,15 @@ function containsAny(text: string, terms: string[]): boolean {
|
|
|
27
28
|
return terms.some((term) => text.includes(term))
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
function dedupe(values: string[]): string[] {
|
|
32
|
+
return Array.from(new Set(values.filter(Boolean)))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function preferredToolsForCapabilities(enabledPlugins: string[], capabilities: string[], fallback: string[] = []): string[] {
|
|
36
|
+
const preferred = capabilities.flatMap((capability) => getToolsForCapability(enabledPlugins, capability))
|
|
37
|
+
return dedupe(preferred.length > 0 ? preferred : fallback)
|
|
38
|
+
}
|
|
39
|
+
|
|
30
40
|
function normalizeDelegateOrder(value: unknown): DelegateTool[] {
|
|
31
41
|
const fallback: DelegateTool[] = [
|
|
32
42
|
'delegate_to_claude_code',
|
|
@@ -59,6 +69,14 @@ export function routeTaskIntent(
|
|
|
59
69
|
const text = (message || '').toLowerCase()
|
|
60
70
|
const url = findFirstUrl(message || '')
|
|
61
71
|
const delegateOrder = normalizeDelegateOrder(settings?.autonomyPreferredDelegates)
|
|
72
|
+
const matchedCapabilities = matchToolCapabilitiesForMessage(enabledPlugins, message)
|
|
73
|
+
const wantsVoiceNote = matchedCapabilities.has(TOOL_CAPABILITY.deliveryVoiceNote)
|
|
74
|
+
const wantsScreenshots = matchedCapabilities.has(TOOL_CAPABILITY.browserCapture)
|
|
75
|
+
const wantsMediaDelivery = matchedCapabilities.has(TOOL_CAPABILITY.deliveryMedia)
|
|
76
|
+
const wantsChannelDelivery = matchedCapabilities.has(TOOL_CAPABILITY.deliveryMessage)
|
|
77
|
+
const researchLike = matchedCapabilities.has(TOOL_CAPABILITY.researchSearch)
|
|
78
|
+
|| matchedCapabilities.has(TOOL_CAPABILITY.researchFetch)
|
|
79
|
+
|| !!url
|
|
62
80
|
|
|
63
81
|
const coding = containsAny(text, [
|
|
64
82
|
'build',
|
|
@@ -98,12 +116,20 @@ export function routeTaskIntent(
|
|
|
98
116
|
'discord',
|
|
99
117
|
'notify',
|
|
100
118
|
'broadcast',
|
|
101
|
-
])
|
|
119
|
+
]) || (!researchLike && (wantsVoiceNote || wantsMediaDelivery || wantsChannelDelivery))
|
|
102
120
|
if (outreach) {
|
|
103
121
|
return {
|
|
104
122
|
intent: 'outreach',
|
|
105
123
|
confidence: 0.8,
|
|
106
|
-
preferredTools:
|
|
124
|
+
preferredTools: preferredToolsForCapabilities(
|
|
125
|
+
enabledPlugins,
|
|
126
|
+
[
|
|
127
|
+
TOOL_CAPABILITY.deliveryVoiceNote,
|
|
128
|
+
TOOL_CAPABILITY.deliveryMedia,
|
|
129
|
+
TOOL_CAPABILITY.deliveryMessage,
|
|
130
|
+
],
|
|
131
|
+
['connector_message_tool', 'manage_connectors', 'manage_sessions'],
|
|
132
|
+
),
|
|
107
133
|
preferredDelegates: delegateOrder,
|
|
108
134
|
primaryUrl: url,
|
|
109
135
|
}
|
|
@@ -129,36 +155,46 @@ export function routeTaskIntent(
|
|
|
129
155
|
}
|
|
130
156
|
|
|
131
157
|
const browsing = !!url && (
|
|
132
|
-
|
|
133
|
-
||
|
|
158
|
+
matchedCapabilities.has(TOOL_CAPABILITY.browserNavigate)
|
|
159
|
+
|| matchedCapabilities.has(TOOL_CAPABILITY.browserCapture)
|
|
160
|
+
|| getToolsForCapability(enabledPlugins, TOOL_CAPABILITY.browserNavigate).length > 0
|
|
161
|
+
|| getToolsForCapability(enabledPlugins, TOOL_CAPABILITY.browserCapture).length > 0
|
|
134
162
|
)
|
|
135
163
|
if (browsing) {
|
|
136
164
|
return {
|
|
137
165
|
intent: 'browsing',
|
|
138
166
|
confidence: 0.7,
|
|
139
|
-
preferredTools:
|
|
167
|
+
preferredTools: preferredToolsForCapabilities(
|
|
168
|
+
enabledPlugins,
|
|
169
|
+
[
|
|
170
|
+
TOOL_CAPABILITY.browserCapture,
|
|
171
|
+
TOOL_CAPABILITY.browserNavigate,
|
|
172
|
+
TOOL_CAPABILITY.researchFetch,
|
|
173
|
+
],
|
|
174
|
+
['browser', 'web_fetch'],
|
|
175
|
+
),
|
|
140
176
|
preferredDelegates: delegateOrder,
|
|
141
177
|
primaryUrl: url,
|
|
142
178
|
}
|
|
143
179
|
}
|
|
144
180
|
|
|
145
|
-
const research =
|
|
146
|
-
'research',
|
|
147
|
-
'look up',
|
|
148
|
-
'find out',
|
|
149
|
-
'search for',
|
|
150
|
-
'compare',
|
|
151
|
-
'latest',
|
|
152
|
-
'news',
|
|
153
|
-
'wikipedia',
|
|
154
|
-
'summarize this url',
|
|
155
|
-
'analyze website',
|
|
156
|
-
]) || !!url
|
|
181
|
+
const research = researchLike
|
|
157
182
|
if (research) {
|
|
183
|
+
const preferred = preferredToolsForCapabilities(
|
|
184
|
+
enabledPlugins,
|
|
185
|
+
[
|
|
186
|
+
TOOL_CAPABILITY.researchSearch,
|
|
187
|
+
TOOL_CAPABILITY.researchFetch,
|
|
188
|
+
...(wantsScreenshots ? [TOOL_CAPABILITY.browserCapture] : []),
|
|
189
|
+
...(wantsVoiceNote ? [TOOL_CAPABILITY.deliveryVoiceNote] : []),
|
|
190
|
+
...(wantsMediaDelivery || wantsChannelDelivery ? [TOOL_CAPABILITY.deliveryMedia, TOOL_CAPABILITY.deliveryMessage] : []),
|
|
191
|
+
],
|
|
192
|
+
['web_search', 'web_fetch', 'browser'],
|
|
193
|
+
)
|
|
158
194
|
return {
|
|
159
195
|
intent: 'research',
|
|
160
196
|
confidence: 0.7,
|
|
161
|
-
preferredTools:
|
|
197
|
+
preferredTools: preferred,
|
|
162
198
|
preferredDelegates: delegateOrder,
|
|
163
199
|
primaryUrl: url,
|
|
164
200
|
}
|
|
@@ -18,7 +18,7 @@ import { getProvider } from '@/lib/providers'
|
|
|
18
18
|
import { estimateCost, checkAgentBudgetLimits } from './cost'
|
|
19
19
|
import { log } from './logger'
|
|
20
20
|
import { logExecution } from './execution-log'
|
|
21
|
-
import { streamAgentChat } from './stream-agent-chat'
|
|
21
|
+
import { buildToolDisciplineLines, streamAgentChat } from './stream-agent-chat'
|
|
22
22
|
import { runLinkUnderstanding } from './link-understanding'
|
|
23
23
|
import { buildSessionTools } from './session-tools'
|
|
24
24
|
import type { StructuredToolInterface } from '@langchain/core/tools'
|
|
@@ -46,6 +46,7 @@ import { buildIdentityContinuityContext, refreshSessionIdentityState } from './i
|
|
|
46
46
|
import { syncSessionArchiveMemory } from './session-archive-memory'
|
|
47
47
|
import { evaluateSessionFreshness, resetSessionRuntime, resolveSessionResetPolicy } from './session-reset-policy'
|
|
48
48
|
import { pruneStreamingAssistantArtifacts, upsertStreamingAssistantArtifact } from '@/lib/chat-streaming-state'
|
|
49
|
+
import { resolveActiveProjectContext } from './project-context'
|
|
49
50
|
type DelegateTool = 'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli' | 'delegate_to_gemini_cli'
|
|
50
51
|
|
|
51
52
|
/** Slice history from the most recent context-clear marker forward */
|
|
@@ -191,6 +192,8 @@ function extractDelegateResponse(outputText: string): string | null {
|
|
|
191
192
|
const MANAGE_PLATFORM_RESOURCE_TO_TOOL: Record<string, string> = {
|
|
192
193
|
agent: 'manage_agents',
|
|
193
194
|
agents: 'manage_agents',
|
|
195
|
+
project: 'manage_projects',
|
|
196
|
+
projects: 'manage_projects',
|
|
194
197
|
task: 'manage_tasks',
|
|
195
198
|
tasks: 'manage_tasks',
|
|
196
199
|
schedule: 'manage_schedules',
|
|
@@ -671,6 +674,12 @@ function syncSessionFromAgent(sessionId: string): void {
|
|
|
671
674
|
}
|
|
672
675
|
const isShortcutChat = session.shortcutForAgentId === agent.id || agent.threadSessionId === sessionId
|
|
673
676
|
if (isShortcutChat) {
|
|
677
|
+
const desiredPlugins = Array.isArray(agent.plugins) ? [...agent.plugins] : []
|
|
678
|
+
const currentPlugins = Array.isArray(session.plugins) ? [...session.plugins] : []
|
|
679
|
+
if (JSON.stringify(currentPlugins) !== JSON.stringify(desiredPlugins)) {
|
|
680
|
+
session.plugins = desiredPlugins
|
|
681
|
+
changed = true
|
|
682
|
+
}
|
|
674
683
|
if (session.shortcutForAgentId !== agent.id) { session.shortcutForAgentId = agent.id; changed = true }
|
|
675
684
|
if (session.name !== agent.name) { session.name = agent.name; changed = true }
|
|
676
685
|
}
|
|
@@ -737,6 +746,14 @@ function buildAgentSystemPrompt(session: Session): string | undefined {
|
|
|
737
746
|
]
|
|
738
747
|
parts.push(thinkingHint.join('\n'))
|
|
739
748
|
|
|
749
|
+
const enabledPlugins = Array.isArray(session.plugins) ? session.plugins : (Array.isArray(agent.plugins) ? agent.plugins : [])
|
|
750
|
+
const toolDisciplineLines = buildToolDisciplineLines(enabledPlugins)
|
|
751
|
+
if (toolDisciplineLines.length > 0) parts.push(['## Tool Discipline', ...toolDisciplineLines].join('\n'))
|
|
752
|
+
const operatingGuidance = getPluginManager().collectOperatingGuidance(enabledPlugins)
|
|
753
|
+
if (operatingGuidance.length > 0) parts.push(['## Tool Guidance', ...operatingGuidance].join('\n'))
|
|
754
|
+
const capabilityLines = getPluginManager().collectCapabilityDescriptions(enabledPlugins)
|
|
755
|
+
if (capabilityLines.length > 0) parts.push(['## Tool Capabilities', ...capabilityLines].join('\n'))
|
|
756
|
+
|
|
740
757
|
// 7. Heartbeat Guidance
|
|
741
758
|
parts.push([
|
|
742
759
|
'## Heartbeats',
|
|
@@ -1261,12 +1278,18 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1261
1278
|
return false
|
|
1262
1279
|
}
|
|
1263
1280
|
const agent = session.agentId ? loadAgents()[session.agentId] : null
|
|
1281
|
+
const activeProjectContext = resolveActiveProjectContext(session)
|
|
1264
1282
|
const { tools, cleanup } = await buildSessionTools(session.cwd, sessionForRun.plugins || sessionForRun.tools || [], {
|
|
1265
1283
|
agentId: session.agentId || null,
|
|
1266
1284
|
sessionId,
|
|
1267
1285
|
platformAssignScope: agent?.platformAssignScope || 'self',
|
|
1268
1286
|
mcpServerIds: agent?.mcpServerIds,
|
|
1269
1287
|
mcpDisabledTools: agent?.mcpDisabledTools,
|
|
1288
|
+
projectId: activeProjectContext.projectId,
|
|
1289
|
+
projectRoot: activeProjectContext.projectRoot,
|
|
1290
|
+
projectName: activeProjectContext.project?.name || null,
|
|
1291
|
+
projectDescription: activeProjectContext.project?.description || null,
|
|
1292
|
+
memoryScopeMode: (((session as unknown as Record<string, unknown>).memoryScopeMode as string | null | undefined) ?? agent?.memoryScopeMode ?? null),
|
|
1270
1293
|
})
|
|
1271
1294
|
try {
|
|
1272
1295
|
const directTool = tools.find((t) => t?.name === toolName) as StructuredToolInterface | undefined
|
|
@@ -1508,6 +1531,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1508
1531
|
claudeCode: normalizeResumeId(sr.claudeCode ?? cr.claudeCode),
|
|
1509
1532
|
codex: normalizeResumeId(sr.codex ?? cr.codex),
|
|
1510
1533
|
opencode: normalizeResumeId(sr.opencode ?? cr.opencode),
|
|
1534
|
+
gemini: normalizeResumeId(sr.gemini ?? cr.gemini),
|
|
1511
1535
|
}
|
|
1512
1536
|
if (JSON.stringify(currentResume) !== JSON.stringify(nextResume)) {
|
|
1513
1537
|
current.delegateResumeIds = nextResume
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { test } from 'node:test'
|
|
3
|
+
import {
|
|
4
|
+
advanceConnectorReconnectState,
|
|
5
|
+
createConnectorReconnectState,
|
|
6
|
+
} from './manager'
|
|
7
|
+
|
|
8
|
+
test('advanceConnectorReconnectState applies exponential backoff and exhaustion', () => {
|
|
9
|
+
const policy = {
|
|
10
|
+
initialBackoffMs: 30_000,
|
|
11
|
+
maxBackoffMs: 15 * 60 * 1000,
|
|
12
|
+
maxAttempts: 3,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const initial = createConnectorReconnectState({}, policy)
|
|
16
|
+
|
|
17
|
+
const first = advanceConnectorReconnectState(initial, 'boom-1', 1_000, policy)
|
|
18
|
+
assert.equal(first.attempts, 1)
|
|
19
|
+
assert.equal(first.backoffMs, 30_000)
|
|
20
|
+
assert.equal(first.nextRetryAt, 31_000)
|
|
21
|
+
assert.equal(first.exhausted, false)
|
|
22
|
+
|
|
23
|
+
const second = advanceConnectorReconnectState(first, 'boom-2', 31_000, policy)
|
|
24
|
+
assert.equal(second.attempts, 2)
|
|
25
|
+
assert.equal(second.backoffMs, 60_000)
|
|
26
|
+
assert.equal(second.nextRetryAt, 91_000)
|
|
27
|
+
assert.equal(second.exhausted, false)
|
|
28
|
+
|
|
29
|
+
const third = advanceConnectorReconnectState(second, 'boom-3', 91_000, policy)
|
|
30
|
+
assert.equal(third.attempts, 3)
|
|
31
|
+
assert.equal(third.backoffMs, 120_000)
|
|
32
|
+
assert.equal(third.nextRetryAt, 211_000)
|
|
33
|
+
assert.equal(third.exhausted, true)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('createConnectorReconnectState respects custom initial backoff', () => {
|
|
37
|
+
const state = createConnectorReconnectState(
|
|
38
|
+
{ error: 'seeded' },
|
|
39
|
+
{ initialBackoffMs: 45_000 },
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
assert.equal(state.attempts, 0)
|
|
43
|
+
assert.equal(state.backoffMs, 45_000)
|
|
44
|
+
assert.equal(state.nextRetryAt, 0)
|
|
45
|
+
assert.equal(state.error, 'seeded')
|
|
46
|
+
assert.equal(state.exhausted, false)
|
|
47
|
+
})
|