@swarmclawai/swarmclaw 0.6.4 → 0.6.7
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 +62 -30
- package/package.json +10 -1
- package/src/app/api/agents/[id]/clone/route.ts +40 -0
- package/src/app/api/agents/route.ts +39 -14
- package/src/app/api/chatrooms/[id]/chat/route.ts +58 -3
- package/src/app/api/chatrooms/[id]/moderate/route.ts +150 -0
- package/src/app/api/chatrooms/[id]/route.ts +34 -2
- package/src/app/api/chatrooms/route.ts +26 -3
- package/src/app/api/connectors/[id]/health/route.ts +64 -0
- package/src/app/api/connectors/route.ts +17 -2
- package/src/app/api/knowledge/route.ts +6 -1
- package/src/app/api/openclaw/doctor/route.ts +17 -0
- package/src/app/api/schedules/[id]/run/route.ts +3 -0
- package/src/app/api/sessions/[id]/chat/route.ts +5 -1
- package/src/app/api/sessions/route.ts +11 -2
- package/src/app/api/tasks/[id]/route.ts +18 -13
- package/src/app/api/tasks/route.ts +44 -1
- package/src/app/api/usage/route.ts +16 -7
- package/src/app/api/wallets/[id]/approve/route.ts +62 -0
- package/src/app/api/wallets/[id]/balance-history/route.ts +18 -0
- package/src/app/api/wallets/[id]/route.ts +118 -0
- package/src/app/api/wallets/[id]/send/route.ts +118 -0
- package/src/app/api/wallets/[id]/transactions/route.ts +18 -0
- package/src/app/api/wallets/route.ts +74 -0
- package/src/app/globals.css +8 -0
- package/src/cli/index.js +20 -0
- package/src/cli/index.ts +223 -39
- package/src/cli/spec.js +14 -0
- package/src/components/agents/agent-avatar.tsx +15 -1
- package/src/components/agents/agent-card.tsx +38 -6
- package/src/components/agents/agent-chat-list.tsx +79 -3
- package/src/components/agents/agent-sheet.tsx +191 -26
- package/src/components/auth/setup-wizard.tsx +268 -353
- package/src/components/chat/chat-area.tsx +24 -9
- package/src/components/chat/chat-header.tsx +48 -19
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/delegation-banner.test.ts +27 -0
- package/src/components/chat/delegation-banner.tsx +109 -23
- package/src/components/chat/message-bubble.tsx +17 -16
- package/src/components/chat/message-list.tsx +6 -5
- package/src/components/chat/streaming-bubble.tsx +3 -2
- package/src/components/chat/thinking-indicator.tsx +3 -2
- package/src/components/chat/transfer-agent-picker.tsx +1 -1
- package/src/components/chatrooms/agent-hover-card.tsx +1 -1
- package/src/components/chatrooms/chatroom-input.tsx +1 -1
- package/src/components/chatrooms/chatroom-message.tsx +165 -23
- package/src/components/chatrooms/chatroom-sheet.tsx +289 -4
- package/src/components/chatrooms/chatroom-typing-bar.tsx +1 -1
- package/src/components/chatrooms/chatroom-view.tsx +62 -17
- package/src/components/connectors/connector-health.tsx +120 -0
- package/src/components/connectors/connector-list.tsx +1 -1
- package/src/components/connectors/connector-sheet.tsx +9 -0
- package/src/components/home/home-view.tsx +25 -3
- package/src/components/input/chat-input.tsx +8 -1
- package/src/components/knowledge/knowledge-list.tsx +1 -1
- package/src/components/knowledge/knowledge-sheet.tsx +1 -1
- package/src/components/layout/app-layout.tsx +35 -4
- package/src/components/memory/memory-agent-list.tsx +1 -1
- package/src/components/memory/memory-browser.tsx +1 -0
- package/src/components/memory/memory-card.tsx +3 -2
- package/src/components/memory/memory-detail.tsx +3 -3
- package/src/components/memory/memory-sheet.tsx +2 -2
- package/src/components/projects/project-detail.tsx +4 -4
- package/src/components/schedules/schedule-list.tsx +55 -9
- package/src/components/schedules/schedule-sheet.tsx +134 -23
- package/src/components/secrets/secret-sheet.tsx +1 -1
- package/src/components/secrets/secrets-list.tsx +1 -1
- package/src/components/sessions/session-card.tsx +1 -1
- package/src/components/shared/agent-picker-list.tsx +1 -1
- package/src/components/shared/agent-switch-dialog.tsx +1 -1
- package/src/components/shared/command-palette.tsx +237 -0
- package/src/components/shared/connector-platform-icon.tsx +1 -0
- package/src/components/shared/settings/section-user-preferences.tsx +4 -4
- package/src/components/skills/skill-list.tsx +1 -1
- package/src/components/skills/skill-sheet.tsx +1 -1
- package/src/components/tasks/task-board.tsx +3 -3
- package/src/components/tasks/task-card.tsx +22 -2
- package/src/components/tasks/task-sheet.tsx +112 -17
- package/src/components/usage/metrics-dashboard.tsx +13 -25
- package/src/components/wallets/wallet-approval-dialog.tsx +99 -0
- package/src/components/wallets/wallet-panel.tsx +616 -0
- package/src/components/wallets/wallet-section.tsx +100 -0
- package/src/hooks/use-swipe.ts +49 -0
- package/src/lib/providers/anthropic.ts +16 -2
- package/src/lib/providers/claude-cli.ts +7 -1
- package/src/lib/providers/index.ts +7 -0
- package/src/lib/providers/ollama.ts +16 -2
- package/src/lib/providers/openai.ts +7 -2
- package/src/lib/providers/openclaw.ts +6 -1
- package/src/lib/providers/provider-defaults.ts +7 -0
- package/src/lib/schedule-templates.ts +115 -0
- package/src/lib/server/agent-registry.ts +2 -2
- package/src/lib/server/alert-dispatch.ts +64 -0
- package/src/lib/server/chat-execution.ts +76 -4
- package/src/lib/server/chatroom-health.ts +60 -0
- package/src/lib/server/chatroom-helpers.test.ts +94 -0
- package/src/lib/server/chatroom-helpers.ts +86 -12
- package/src/lib/server/chatroom-routing.ts +65 -0
- package/src/lib/server/connectors/discord.ts +3 -0
- package/src/lib/server/connectors/email.ts +267 -0
- package/src/lib/server/connectors/inbound-audio-transcription.test.ts +191 -0
- package/src/lib/server/connectors/inbound-audio-transcription.ts +261 -0
- package/src/lib/server/connectors/manager.ts +239 -5
- package/src/lib/server/connectors/openclaw.ts +3 -0
- package/src/lib/server/connectors/slack.ts +6 -0
- package/src/lib/server/connectors/telegram.ts +18 -0
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp-text.test.ts +29 -0
- package/src/lib/server/connectors/whatsapp-text.ts +26 -0
- package/src/lib/server/connectors/whatsapp.ts +17 -5
- package/src/lib/server/cost.ts +70 -0
- package/src/lib/server/create-notification.ts +2 -0
- package/src/lib/server/daemon-state.ts +124 -0
- package/src/lib/server/dag-validation.ts +115 -0
- package/src/lib/server/memory-db.ts +12 -7
- package/src/lib/server/openclaw-doctor.ts +48 -0
- package/src/lib/server/orchestrator-lg.ts +12 -2
- package/src/lib/server/orchestrator.ts +6 -1
- package/src/lib/server/queue-followups.test.ts +224 -0
- package/src/lib/server/queue.ts +238 -24
- package/src/lib/server/scheduler.ts +3 -0
- package/src/lib/server/session-run-manager.ts +22 -1
- package/src/lib/server/session-tools/chatroom.ts +11 -2
- package/src/lib/server/session-tools/context-mgmt.ts +2 -2
- package/src/lib/server/session-tools/index.ts +8 -2
- package/src/lib/server/session-tools/memory.ts +23 -4
- package/src/lib/server/session-tools/openclaw-workspace.ts +132 -0
- package/src/lib/server/session-tools/shell.ts +1 -1
- package/src/lib/server/session-tools/wallet.ts +124 -0
- package/src/lib/server/session-tools/web.ts +2 -2
- package/src/lib/server/solana.ts +122 -0
- package/src/lib/server/storage.ts +158 -6
- package/src/lib/server/stream-agent-chat.ts +126 -63
- package/src/lib/server/task-mention.test.ts +41 -0
- package/src/lib/server/task-mention.ts +3 -2
- package/src/lib/setup-defaults.ts +277 -0
- package/src/lib/tool-definitions.ts +1 -0
- package/src/lib/validation/schemas.ts +69 -0
- package/src/lib/view-routes.ts +1 -0
- package/src/stores/use-app-store.ts +15 -3
- package/src/stores/use-chatroom-store.ts +52 -2
- package/src/types/index.ts +98 -2
- package/tsconfig.json +2 -1
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import type { BoardTask } from '@/types'
|
|
4
|
+
import { resolveTaskOriginConnectorFollowupTarget } from './queue'
|
|
5
|
+
|
|
6
|
+
function makeTask(partial?: Partial<BoardTask> & { createdInSessionId?: string | null }): BoardTask {
|
|
7
|
+
const now = Date.now()
|
|
8
|
+
return {
|
|
9
|
+
id: 'task-1',
|
|
10
|
+
title: 'Test task',
|
|
11
|
+
description: 'desc',
|
|
12
|
+
status: 'queued',
|
|
13
|
+
agentId: 'agent-a',
|
|
14
|
+
createdAt: now,
|
|
15
|
+
updatedAt: now,
|
|
16
|
+
...(partial || {}),
|
|
17
|
+
} as BoardTask
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type SessionFixtureMap = Record<string, {
|
|
21
|
+
messages: Array<{
|
|
22
|
+
role: string
|
|
23
|
+
text?: string
|
|
24
|
+
source?: {
|
|
25
|
+
connectorId?: string
|
|
26
|
+
channelId?: string
|
|
27
|
+
}
|
|
28
|
+
}>
|
|
29
|
+
}>
|
|
30
|
+
|
|
31
|
+
describe('resolveTaskOriginConnectorFollowupTarget', () => {
|
|
32
|
+
it('uses connector source channel from origin session and normalizes WhatsApp numbers', () => {
|
|
33
|
+
const task = makeTask({ createdInSessionId: 'session-1' })
|
|
34
|
+
const sessions = {
|
|
35
|
+
'session-1': {
|
|
36
|
+
messages: [
|
|
37
|
+
{ role: 'assistant', text: 'ok' },
|
|
38
|
+
{
|
|
39
|
+
role: 'user',
|
|
40
|
+
text: 'please update me',
|
|
41
|
+
source: {
|
|
42
|
+
connectorId: 'conn-wa',
|
|
43
|
+
channelId: '+44 7700 900123',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
const connectors = {
|
|
50
|
+
'conn-wa': {
|
|
51
|
+
id: 'conn-wa',
|
|
52
|
+
platform: 'whatsapp',
|
|
53
|
+
agentId: 'agent-a',
|
|
54
|
+
config: {},
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
const running = [
|
|
58
|
+
{
|
|
59
|
+
id: 'conn-wa',
|
|
60
|
+
platform: 'whatsapp',
|
|
61
|
+
agentId: 'agent-a',
|
|
62
|
+
supportsSend: true,
|
|
63
|
+
configuredTargets: [],
|
|
64
|
+
recentChannelId: '185200000000000@lid',
|
|
65
|
+
},
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
const target = resolveTaskOriginConnectorFollowupTarget({
|
|
69
|
+
task,
|
|
70
|
+
sessions: sessions as SessionFixtureMap,
|
|
71
|
+
connectors,
|
|
72
|
+
running,
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
assert.deepEqual(target, {
|
|
76
|
+
connectorId: 'conn-wa',
|
|
77
|
+
channelId: '447700900123@s.whatsapp.net',
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('falls back to runtime recent channel when source channel is unavailable', () => {
|
|
82
|
+
const task = makeTask({ createdInSessionId: 'session-1' })
|
|
83
|
+
const sessions = {
|
|
84
|
+
'session-1': {
|
|
85
|
+
messages: [
|
|
86
|
+
{
|
|
87
|
+
role: 'user',
|
|
88
|
+
text: 'run this later',
|
|
89
|
+
source: {
|
|
90
|
+
connectorId: 'conn-telegram',
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
const connectors = {
|
|
97
|
+
'conn-telegram': {
|
|
98
|
+
id: 'conn-telegram',
|
|
99
|
+
platform: 'telegram',
|
|
100
|
+
agentId: 'agent-a',
|
|
101
|
+
config: {},
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
const running = [
|
|
105
|
+
{
|
|
106
|
+
id: 'conn-telegram',
|
|
107
|
+
platform: 'telegram',
|
|
108
|
+
agentId: 'agent-a',
|
|
109
|
+
supportsSend: true,
|
|
110
|
+
configuredTargets: [],
|
|
111
|
+
recentChannelId: 'tg-chat-42',
|
|
112
|
+
},
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
const target = resolveTaskOriginConnectorFollowupTarget({
|
|
116
|
+
task,
|
|
117
|
+
sessions: sessions as SessionFixtureMap,
|
|
118
|
+
connectors,
|
|
119
|
+
running,
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
assert.deepEqual(target, {
|
|
123
|
+
connectorId: 'conn-telegram',
|
|
124
|
+
channelId: 'tg-chat-42',
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('returns null when the source connector belongs to a different agent', () => {
|
|
129
|
+
const task = makeTask({ createdInSessionId: 'session-1' })
|
|
130
|
+
const sessions = {
|
|
131
|
+
'session-1': {
|
|
132
|
+
messages: [
|
|
133
|
+
{
|
|
134
|
+
role: 'user',
|
|
135
|
+
text: 'do it',
|
|
136
|
+
source: {
|
|
137
|
+
connectorId: 'conn-wa',
|
|
138
|
+
channelId: '+15551230000',
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
}
|
|
144
|
+
const connectors = {
|
|
145
|
+
'conn-wa': {
|
|
146
|
+
id: 'conn-wa',
|
|
147
|
+
platform: 'whatsapp',
|
|
148
|
+
agentId: 'different-agent',
|
|
149
|
+
config: {},
|
|
150
|
+
},
|
|
151
|
+
}
|
|
152
|
+
const running = [
|
|
153
|
+
{
|
|
154
|
+
id: 'conn-wa',
|
|
155
|
+
platform: 'whatsapp',
|
|
156
|
+
agentId: 'different-agent',
|
|
157
|
+
supportsSend: true,
|
|
158
|
+
configuredTargets: [],
|
|
159
|
+
recentChannelId: null,
|
|
160
|
+
},
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
const target = resolveTaskOriginConnectorFollowupTarget({
|
|
164
|
+
task,
|
|
165
|
+
sessions: sessions as SessionFixtureMap,
|
|
166
|
+
connectors,
|
|
167
|
+
running,
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
assert.equal(target, null)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('allows delegated tasks to follow up via the delegating agent connector', () => {
|
|
174
|
+
const task = makeTask({
|
|
175
|
+
agentId: 'worker-agent',
|
|
176
|
+
delegatedByAgentId: 'delegator-agent',
|
|
177
|
+
createdInSessionId: 'session-1',
|
|
178
|
+
})
|
|
179
|
+
const sessions = {
|
|
180
|
+
'session-1': {
|
|
181
|
+
messages: [
|
|
182
|
+
{
|
|
183
|
+
role: 'user',
|
|
184
|
+
text: 'run and update me here',
|
|
185
|
+
source: {
|
|
186
|
+
connectorId: 'conn-wa',
|
|
187
|
+
channelId: '+44 7700 900123',
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
}
|
|
193
|
+
const connectors = {
|
|
194
|
+
'conn-wa': {
|
|
195
|
+
id: 'conn-wa',
|
|
196
|
+
platform: 'whatsapp',
|
|
197
|
+
agentId: 'delegator-agent',
|
|
198
|
+
config: {},
|
|
199
|
+
},
|
|
200
|
+
}
|
|
201
|
+
const running = [
|
|
202
|
+
{
|
|
203
|
+
id: 'conn-wa',
|
|
204
|
+
platform: 'whatsapp',
|
|
205
|
+
agentId: 'delegator-agent',
|
|
206
|
+
supportsSend: true,
|
|
207
|
+
configuredTargets: [],
|
|
208
|
+
recentChannelId: null,
|
|
209
|
+
},
|
|
210
|
+
]
|
|
211
|
+
|
|
212
|
+
const target = resolveTaskOriginConnectorFollowupTarget({
|
|
213
|
+
task,
|
|
214
|
+
sessions: sessions as SessionFixtureMap,
|
|
215
|
+
connectors,
|
|
216
|
+
running,
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
assert.deepEqual(target, {
|
|
220
|
+
connectorId: 'conn-wa',
|
|
221
|
+
channelId: '447700900123@s.whatsapp.net',
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
})
|
package/src/lib/server/queue.ts
CHANGED
|
@@ -12,7 +12,8 @@ import { executeSessionChatTurn } from './chat-execution'
|
|
|
12
12
|
import { extractTaskResult, formatResultBody } from './task-result'
|
|
13
13
|
import { getCheckpointSaver } from './langgraph-checkpoint'
|
|
14
14
|
import { isProtectedMainSession } from './main-session'
|
|
15
|
-
import
|
|
15
|
+
import { cascadeUnblock } from './dag-validation'
|
|
16
|
+
import type { Agent, BoardTask, Connector, Message } from '@/types'
|
|
16
17
|
|
|
17
18
|
// HMR-safe: pin processing flag to globalThis so hot reloads don't reset it
|
|
18
19
|
const _queueState = ((globalThis as Record<string, unknown>).__swarmclaw_queue__ ??= { processing: false, pendingKick: false }) as { processing: boolean; pendingKick: boolean }
|
|
@@ -22,6 +23,10 @@ interface SessionMessageLike {
|
|
|
22
23
|
text?: string
|
|
23
24
|
time?: number
|
|
24
25
|
kind?: 'chat' | 'heartbeat' | 'system' | 'context-clear'
|
|
26
|
+
source?: {
|
|
27
|
+
connectorId?: string
|
|
28
|
+
channelId?: string
|
|
29
|
+
}
|
|
25
30
|
toolEvents?: Array<{ name?: string; output?: string }>
|
|
26
31
|
}
|
|
27
32
|
|
|
@@ -39,6 +44,20 @@ interface ScheduleTaskMeta extends BoardTask {
|
|
|
39
44
|
createdByAgentId?: string | null
|
|
40
45
|
}
|
|
41
46
|
|
|
47
|
+
interface RunningConnectorLike {
|
|
48
|
+
id: string
|
|
49
|
+
platform: string
|
|
50
|
+
agentId: string | null
|
|
51
|
+
supportsSend: boolean
|
|
52
|
+
configuredTargets: string[]
|
|
53
|
+
recentChannelId: string | null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface ConnectorTaskFollowupTarget {
|
|
57
|
+
connectorId: string
|
|
58
|
+
channelId: string
|
|
59
|
+
}
|
|
60
|
+
|
|
42
61
|
function sameReasons(a?: string[] | null, b?: string[] | null): boolean {
|
|
43
62
|
const av = Array.isArray(a) ? a : []
|
|
44
63
|
const bv = Array.isArray(b) ? b : []
|
|
@@ -164,6 +183,135 @@ function maybeResolveUploadMediaPathFromUrl(url: string | undefined): string | u
|
|
|
164
183
|
return fs.existsSync(fullPath) ? fullPath : undefined
|
|
165
184
|
}
|
|
166
185
|
|
|
186
|
+
const OUTPUT_FILE_BACKTICK_RE = /`([^`\n]+\.(?:txt|md|json|csv|pdf|png|jpe?g|webp|gif|svg|mp4|webm|mov|zip|tar|gz|log|yml|yaml|xml|html|css|js|ts|tsx|jsx|py|go|rs|java|swift|kt|sql))`/gi
|
|
187
|
+
const OUTPUT_FILE_PATH_RE = /\b((?:\.{1,2}\/|~\/|\/)?[\w./-]+\.(?:txt|md|json|csv|pdf|png|jpe?g|webp|gif|svg|mp4|webm|mov|zip|tar|gz|log|yml|yaml|xml|html|css|js|ts|tsx|jsx|py|go|rs|java|swift|kt|sql))\b/gi
|
|
188
|
+
const MAX_CONNECTOR_ATTACHMENT_BYTES = 25 * 1024 * 1024
|
|
189
|
+
|
|
190
|
+
function extractLikelyOutputFiles(text: string): string[] {
|
|
191
|
+
const out: string[] = []
|
|
192
|
+
const seen = new Set<string>()
|
|
193
|
+
const push = (raw: string) => {
|
|
194
|
+
const value = raw.trim().replace(/^['"]|['"]$/g, '')
|
|
195
|
+
if (!value || /^https?:\/\//i.test(value)) return
|
|
196
|
+
if (value.startsWith('/api/uploads/')) return
|
|
197
|
+
const key = value.toLowerCase()
|
|
198
|
+
if (seen.has(key)) return
|
|
199
|
+
seen.add(key)
|
|
200
|
+
out.push(value)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
for (const match of text.matchAll(OUTPUT_FILE_BACKTICK_RE)) {
|
|
204
|
+
push(match[1] || '')
|
|
205
|
+
if (out.length >= 8) return out
|
|
206
|
+
}
|
|
207
|
+
for (const match of text.matchAll(OUTPUT_FILE_PATH_RE)) {
|
|
208
|
+
push(match[1] || '')
|
|
209
|
+
if (out.length >= 8) return out
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return out
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function resolveExistingOutputFilePath(fileRef: string, cwd: string): string | null {
|
|
216
|
+
const ref = (fileRef || '').trim()
|
|
217
|
+
if (!ref) return null
|
|
218
|
+
if (ref.startsWith('/api/uploads/')) {
|
|
219
|
+
return maybeResolveUploadMediaPathFromUrl(ref) || null
|
|
220
|
+
}
|
|
221
|
+
const withoutFileScheme = ref.replace(/^file:\/\//i, '')
|
|
222
|
+
const candidates = path.isAbsolute(withoutFileScheme)
|
|
223
|
+
? [withoutFileScheme]
|
|
224
|
+
: [
|
|
225
|
+
cwd ? path.resolve(cwd, withoutFileScheme) : '',
|
|
226
|
+
path.resolve(WORKSPACE_DIR, withoutFileScheme),
|
|
227
|
+
].filter(Boolean)
|
|
228
|
+
|
|
229
|
+
for (const candidate of candidates) {
|
|
230
|
+
try {
|
|
231
|
+
const stat = fs.statSync(candidate)
|
|
232
|
+
if (stat.isFile()) return candidate
|
|
233
|
+
} catch {
|
|
234
|
+
// ignore missing candidate
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return null
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function isSendableAttachment(filePath: string): boolean {
|
|
241
|
+
try {
|
|
242
|
+
const stat = fs.statSync(filePath)
|
|
243
|
+
return stat.isFile() && stat.size <= MAX_CONNECTOR_ATTACHMENT_BYTES
|
|
244
|
+
} catch {
|
|
245
|
+
return false
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function resolveTaskOriginConnectorFollowupTarget(params: {
|
|
250
|
+
task: BoardTask
|
|
251
|
+
sessions: Record<string, SessionLike>
|
|
252
|
+
connectors: Record<string, Connector>
|
|
253
|
+
running: RunningConnectorLike[]
|
|
254
|
+
}): ConnectorTaskFollowupTarget | null {
|
|
255
|
+
const { task, sessions, connectors, running } = params
|
|
256
|
+
const metaTask = task as ScheduleTaskMeta
|
|
257
|
+
const delegatedByAgentId = typeof metaTask.delegatedByAgentId === 'string'
|
|
258
|
+
? metaTask.delegatedByAgentId.trim()
|
|
259
|
+
: ''
|
|
260
|
+
const sourceSessionId = typeof metaTask.createdInSessionId === 'string'
|
|
261
|
+
? metaTask.createdInSessionId.trim()
|
|
262
|
+
: ''
|
|
263
|
+
if (!sourceSessionId) return null
|
|
264
|
+
const sourceSession = sessions[sourceSessionId]
|
|
265
|
+
if (!sourceSession || !Array.isArray(sourceSession.messages)) return null
|
|
266
|
+
|
|
267
|
+
const runningById = new Map<string, RunningConnectorLike>()
|
|
268
|
+
for (const entry of running) {
|
|
269
|
+
if (!entry?.id) continue
|
|
270
|
+
runningById.set(entry.id, entry)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
for (let i = sourceSession.messages.length - 1; i >= 0; i--) {
|
|
274
|
+
const message = sourceSession.messages[i]
|
|
275
|
+
if (!message || message.role !== 'user') continue
|
|
276
|
+
|
|
277
|
+
const connectorId = typeof message.source?.connectorId === 'string'
|
|
278
|
+
? message.source.connectorId.trim()
|
|
279
|
+
: ''
|
|
280
|
+
if (!connectorId) continue
|
|
281
|
+
|
|
282
|
+
const connector = connectors[connectorId]
|
|
283
|
+
if (!connector) continue
|
|
284
|
+
const ownerId = typeof connector.agentId === 'string' ? connector.agentId.trim() : ''
|
|
285
|
+
if (ownerId) {
|
|
286
|
+
const allowedOwners = new Set([task.agentId, delegatedByAgentId].filter(Boolean))
|
|
287
|
+
if (!allowedOwners.has(ownerId)) continue
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const runtime = runningById.get(connectorId)
|
|
291
|
+
if (runtime && !runtime.supportsSend) continue
|
|
292
|
+
|
|
293
|
+
const sourceChannel = typeof message.source?.channelId === 'string'
|
|
294
|
+
? message.source.channelId.trim()
|
|
295
|
+
: ''
|
|
296
|
+
const fallbackChannel = runtime?.recentChannelId
|
|
297
|
+
|| runtime?.configuredTargets?.[0]
|
|
298
|
+
|| connector.config?.outboundJid
|
|
299
|
+
|| connector.config?.outboundTarget
|
|
300
|
+
|| ''
|
|
301
|
+
const rawChannel = sourceChannel || fallbackChannel
|
|
302
|
+
if (!rawChannel) continue
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
connectorId,
|
|
306
|
+
channelId: connector.platform === 'whatsapp'
|
|
307
|
+
? normalizeWhatsappTarget(rawChannel)
|
|
308
|
+
: rawChannel,
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return null
|
|
313
|
+
}
|
|
314
|
+
|
|
167
315
|
// Task result extraction now uses Zod-validated structured data
|
|
168
316
|
// from ./task-result.ts (extractTaskResult, formatResultBody)
|
|
169
317
|
|
|
@@ -270,37 +418,67 @@ async function notifyConnectorTaskFollowups(params: {
|
|
|
270
418
|
statusLabel: string
|
|
271
419
|
summaryText: string
|
|
272
420
|
imageUrl?: string
|
|
421
|
+
mediaPath?: string
|
|
422
|
+
mediaFileName?: string
|
|
273
423
|
}) {
|
|
274
|
-
const { task, statusLabel, summaryText, imageUrl } = params
|
|
424
|
+
const { task, statusLabel, summaryText, imageUrl, mediaPath, mediaFileName } = params
|
|
275
425
|
|
|
276
426
|
const connectors = loadConnectors()
|
|
277
427
|
const running = (await import('./connectors/manager')).listRunningConnectors()
|
|
278
428
|
const manager = await import('./connectors/manager')
|
|
429
|
+
const sessions = loadSessions()
|
|
279
430
|
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if (
|
|
285
|
-
|
|
431
|
+
const candidateByKey = new Map<string, ConnectorTaskFollowupTarget>()
|
|
432
|
+
const addCandidate = (candidate: ConnectorTaskFollowupTarget | null | undefined) => {
|
|
433
|
+
if (!candidate?.connectorId || !candidate?.channelId) return
|
|
434
|
+
const key = `${candidate.connectorId}|${candidate.channelId}`
|
|
435
|
+
if (!candidateByKey.has(key)) candidateByKey.set(key, candidate)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const originTarget = resolveTaskOriginConnectorFollowupTarget({
|
|
439
|
+
task,
|
|
440
|
+
sessions: sessions as Record<string, SessionLike>,
|
|
441
|
+
connectors,
|
|
442
|
+
running: running as RunningConnectorLike[],
|
|
286
443
|
})
|
|
287
|
-
|
|
444
|
+
addCandidate(originTarget)
|
|
445
|
+
const preferredTargetKey = originTarget
|
|
446
|
+
? `${originTarget.connectorId}|${originTarget.channelId}`
|
|
447
|
+
: ''
|
|
288
448
|
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
const connector = connectors[
|
|
449
|
+
for (const entry of running) {
|
|
450
|
+
if (!entry.supportsSend || !entry.id) continue
|
|
451
|
+
const connector = connectors[entry.id]
|
|
292
452
|
if (!connector) continue
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
453
|
+
if (connector.agentId !== task.agentId) continue
|
|
454
|
+
if (!isEnabledFlag(connector.config?.taskFollowups)) continue
|
|
455
|
+
const channelTargetRaw = entry.recentChannelId
|
|
456
|
+
|| entry.configuredTargets[0]
|
|
296
457
|
|| connector.config?.outboundJid
|
|
297
458
|
|| connector.config?.outboundTarget
|
|
298
459
|
|| ''
|
|
299
460
|
if (!channelTargetRaw) continue
|
|
461
|
+
addCandidate({
|
|
462
|
+
connectorId: entry.id,
|
|
463
|
+
channelId: connector.platform === 'whatsapp'
|
|
464
|
+
? normalizeWhatsappTarget(channelTargetRaw)
|
|
465
|
+
: channelTargetRaw,
|
|
466
|
+
})
|
|
467
|
+
}
|
|
468
|
+
const targets = [...candidateByKey.values()].sort((a, b) => {
|
|
469
|
+
if (!preferredTargetKey) return 0
|
|
470
|
+
const aKey = `${a.connectorId}|${a.channelId}`
|
|
471
|
+
const bKey = `${b.connectorId}|${b.channelId}`
|
|
472
|
+
if (aKey === preferredTargetKey && bKey !== preferredTargetKey) return -1
|
|
473
|
+
if (bKey === preferredTargetKey && aKey !== preferredTargetKey) return 1
|
|
474
|
+
return 0
|
|
475
|
+
})
|
|
476
|
+
if (!targets.length) return
|
|
300
477
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
478
|
+
const summary = summaryText.trim().slice(0, 1400)
|
|
479
|
+
for (const target of targets) {
|
|
480
|
+
const connector = connectors[target.connectorId]
|
|
481
|
+
if (!connector) continue
|
|
304
482
|
|
|
305
483
|
const template = typeof connector.config?.taskFollowupTemplate === 'string'
|
|
306
484
|
? connector.config.taskFollowupTemplate.trim()
|
|
@@ -316,23 +494,29 @@ async function notifyConnectorTaskFollowups(params: {
|
|
|
316
494
|
`Task ${statusLabel}: ${task.title}`,
|
|
317
495
|
summary || 'No summary provided.',
|
|
318
496
|
].join('\n\n')
|
|
497
|
+
const targetKey = `${target.connectorId}|${target.channelId}`
|
|
498
|
+
const preferredChannelNote = !template && preferredTargetKey && targetKey === preferredTargetKey
|
|
499
|
+
? '\n\n(Update sent in the same channel that requested this task.)'
|
|
500
|
+
: ''
|
|
501
|
+
const outboundMessage = `${message}${preferredChannelNote}`
|
|
319
502
|
|
|
320
|
-
const resolvedMediaPath = maybeResolveUploadMediaPathFromUrl(imageUrl)
|
|
503
|
+
const resolvedMediaPath = mediaPath || maybeResolveUploadMediaPathFromUrl(imageUrl)
|
|
321
504
|
try {
|
|
322
505
|
await manager.sendConnectorMessage({
|
|
323
|
-
connectorId:
|
|
324
|
-
channelId,
|
|
325
|
-
text:
|
|
506
|
+
connectorId: target.connectorId,
|
|
507
|
+
channelId: target.channelId,
|
|
508
|
+
text: outboundMessage,
|
|
326
509
|
...(resolvedMediaPath
|
|
327
510
|
? {
|
|
328
511
|
mediaPath: resolvedMediaPath,
|
|
329
|
-
|
|
512
|
+
fileName: mediaFileName || path.basename(resolvedMediaPath),
|
|
513
|
+
caption: outboundMessage,
|
|
330
514
|
}
|
|
331
515
|
: {}),
|
|
332
516
|
})
|
|
333
517
|
} catch (err: unknown) {
|
|
334
518
|
const errMsg = err instanceof Error ? err.message : String(err)
|
|
335
|
-
console.warn(`[queue] Failed task follow-up send on connector ${
|
|
519
|
+
console.warn(`[queue] Failed task follow-up send on connector ${target.connectorId}: ${errMsg}`)
|
|
336
520
|
}
|
|
337
521
|
}
|
|
338
522
|
}
|
|
@@ -358,10 +542,16 @@ function notifyAgentThreadTaskResult(task: BoardTask): void {
|
|
|
358
542
|
{ sinceTime: typeof task.startedAt === 'number' ? task.startedAt : null },
|
|
359
543
|
)
|
|
360
544
|
const resultBody = formatResultBody(taskResult)
|
|
545
|
+
const outputFileRefs = Array.isArray(task.outputFiles) && task.outputFiles.length > 0
|
|
546
|
+
? task.outputFiles
|
|
547
|
+
: extractLikelyOutputFiles(resultBody)
|
|
361
548
|
|
|
362
549
|
const statusLabel = task.status === 'completed' ? 'completed' : 'failed'
|
|
363
550
|
const taskLink = `[${task.title}](#task:${task.id})`
|
|
364
551
|
const firstImage = taskResult.artifacts.find((a) => a.type === 'image')
|
|
552
|
+
const firstArtifactMediaPath = taskResult.artifacts
|
|
553
|
+
.map((artifact) => maybeResolveUploadMediaPathFromUrl(artifact.url))
|
|
554
|
+
.find((candidate): candidate is string => Boolean(candidate))
|
|
365
555
|
const now = Date.now()
|
|
366
556
|
let changed = false
|
|
367
557
|
|
|
@@ -377,6 +567,11 @@ function notifyAgentThreadTaskResult(task: BoardTask): void {
|
|
|
377
567
|
|
|
378
568
|
// Get working directory from execution session
|
|
379
569
|
const execCwd = runSession?.cwd || ''
|
|
570
|
+
const existingOutputPaths = outputFileRefs
|
|
571
|
+
.map((fileRef) => resolveExistingOutputFilePath(fileRef, execCwd))
|
|
572
|
+
.filter((candidate): candidate is string => Boolean(candidate))
|
|
573
|
+
const firstLocalOutputPath = existingOutputPaths.find((candidate) => isSendableAttachment(candidate))
|
|
574
|
+
const followupMediaPath = firstArtifactMediaPath || firstLocalOutputPath || undefined
|
|
380
575
|
|
|
381
576
|
const buildMsg = (text: string): Message => {
|
|
382
577
|
const msg: Message = { role: 'assistant', text, time: now, kind: 'system' }
|
|
@@ -387,6 +582,10 @@ function notifyAgentThreadTaskResult(task: BoardTask): void {
|
|
|
387
582
|
const buildResultBlock = (prefix: string): string => {
|
|
388
583
|
const parts = [prefix]
|
|
389
584
|
if (execCwd) parts.push(`Working directory: \`${execCwd}\``)
|
|
585
|
+
if (outputFileRefs.length > 0) {
|
|
586
|
+
parts.push(`Output files:\n${outputFileRefs.slice(0, 8).map((fileRef) => `- \`${fileRef}\``).join('\n')}`)
|
|
587
|
+
}
|
|
588
|
+
if (task.completionReportPath) parts.push(`Task report: \`${task.completionReportPath}\``)
|
|
390
589
|
if (resumeLines.length > 0) parts.push(resumeLines.join(' | '))
|
|
391
590
|
parts.push(resultBody || 'No summary.')
|
|
392
591
|
return parts.join('\n\n')
|
|
@@ -449,6 +648,8 @@ function notifyAgentThreadTaskResult(task: BoardTask): void {
|
|
|
449
648
|
statusLabel,
|
|
450
649
|
summaryText: resultBody || '',
|
|
451
650
|
imageUrl: firstImage?.url,
|
|
651
|
+
mediaPath: followupMediaPath,
|
|
652
|
+
mediaFileName: followupMediaPath ? path.basename(followupMediaPath) : undefined,
|
|
452
653
|
})
|
|
453
654
|
}
|
|
454
655
|
|
|
@@ -812,6 +1013,8 @@ export async function processNext() {
|
|
|
812
1013
|
)
|
|
813
1014
|
const enrichedResult = formatResultBody(taskResult)
|
|
814
1015
|
t2[taskId].result = enrichedResult.slice(0, 4000) || null
|
|
1016
|
+
t2[taskId].artifacts = taskResult.artifacts.slice(0, 24)
|
|
1017
|
+
t2[taskId].outputFiles = extractLikelyOutputFiles(enrichedResult).slice(0, 24)
|
|
815
1018
|
t2[taskId].updatedAt = Date.now()
|
|
816
1019
|
const report = ensureTaskCompletionReport(t2[taskId])
|
|
817
1020
|
if (report?.relativePath) t2[taskId].completionReportPath = report.relativePath
|
|
@@ -909,6 +1112,17 @@ export async function processNext() {
|
|
|
909
1112
|
getCheckpointSaver().deleteThread(taskId).catch((e) =>
|
|
910
1113
|
console.warn(`[queue] Failed to clean up checkpoints for task ${taskId}:`, e)
|
|
911
1114
|
)
|
|
1115
|
+
// Cascade unblock: auto-queue tasks whose blockers are all done
|
|
1116
|
+
const latestTasks = loadTasks()
|
|
1117
|
+
const unblockedIds = cascadeUnblock(latestTasks, taskId)
|
|
1118
|
+
if (unblockedIds.length > 0) {
|
|
1119
|
+
saveTasks(latestTasks)
|
|
1120
|
+
for (const uid of unblockedIds) {
|
|
1121
|
+
enqueueTask(uid)
|
|
1122
|
+
console.log(`[queue] Auto-unblocked task "${latestTasks[uid]?.title}" (${uid})`)
|
|
1123
|
+
}
|
|
1124
|
+
notify('tasks')
|
|
1125
|
+
}
|
|
912
1126
|
console.log(`[queue] Task "${task.title}" completed`)
|
|
913
1127
|
} else {
|
|
914
1128
|
if (doneTask?.status === 'queued') {
|
|
@@ -146,7 +146,10 @@ async function tick() {
|
|
|
146
146
|
existingTask.title = `[Sched] ${schedule.name} (run #${schedule.runNumber})`
|
|
147
147
|
existingTask.result = null
|
|
148
148
|
existingTask.error = null
|
|
149
|
+
existingTask.outputFiles = []
|
|
150
|
+
existingTask.artifacts = []
|
|
149
151
|
existingTask.sessionId = null
|
|
152
|
+
existingTask.completionReportPath = null
|
|
150
153
|
existingTask.updatedAt = now
|
|
151
154
|
existingTask.queuedAt = null
|
|
152
155
|
existingTask.startedAt = null
|
|
@@ -85,6 +85,17 @@ function registerRun(run: SessionRunRecord) {
|
|
|
85
85
|
trimRecentRuns()
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/** Chain an external AbortSignal to an internal AbortController so that
|
|
89
|
+
* when the caller (e.g. HTTP request) disconnects, the run is cancelled. */
|
|
90
|
+
function chainCallerSignal(callerSignal: AbortSignal, controller: AbortController): void {
|
|
91
|
+
if (callerSignal.aborted) {
|
|
92
|
+
controller.abort()
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
const onAbort = () => controller.abort()
|
|
96
|
+
callerSignal.addEventListener('abort', onAbort, { once: true })
|
|
97
|
+
}
|
|
98
|
+
|
|
88
99
|
function emitToSubscribers(entry: QueueEntry, event: SSEEvent) {
|
|
89
100
|
for (const send of entry.onEvents) {
|
|
90
101
|
try {
|
|
@@ -347,6 +358,8 @@ export interface EnqueueSessionRunInput {
|
|
|
347
358
|
modelOverride?: string
|
|
348
359
|
heartbeatConfig?: { ackMaxChars: number; showOk: boolean; showAlerts: boolean; target: string | null }
|
|
349
360
|
replyToId?: string
|
|
361
|
+
/** External abort signal (e.g. from the HTTP request) — chained to the run's internal AbortController */
|
|
362
|
+
callerSignal?: AbortSignal
|
|
350
363
|
}
|
|
351
364
|
|
|
352
365
|
export interface EnqueueSessionRunResult {
|
|
@@ -355,6 +368,8 @@ export interface EnqueueSessionRunResult {
|
|
|
355
368
|
deduped?: boolean
|
|
356
369
|
coalesced?: boolean
|
|
357
370
|
promise: Promise<ExecuteChatTurnResult>
|
|
371
|
+
/** Abort the run's internal AbortController (cancels the LLM stream). */
|
|
372
|
+
abort: () => void
|
|
358
373
|
}
|
|
359
374
|
|
|
360
375
|
export function enqueueSessionRun(input: EnqueueSessionRunInput): EnqueueSessionRunResult {
|
|
@@ -371,11 +386,13 @@ export function enqueueSessionRun(input: EnqueueSessionRunInput): EnqueueSession
|
|
|
371
386
|
const dedupe = findDedupeMatch(input.sessionId, input.dedupeKey)
|
|
372
387
|
if (dedupe) {
|
|
373
388
|
if (input.onEvent) dedupe.onEvents.push(input.onEvent)
|
|
389
|
+
if (input.callerSignal) chainCallerSignal(input.callerSignal, dedupe.signalController)
|
|
374
390
|
return {
|
|
375
391
|
runId: dedupe.run.id,
|
|
376
392
|
position: 0,
|
|
377
393
|
deduped: true,
|
|
378
394
|
promise: dedupe.promise,
|
|
395
|
+
abort: () => dedupe.signalController.abort(),
|
|
379
396
|
}
|
|
380
397
|
}
|
|
381
398
|
|
|
@@ -413,12 +430,14 @@ export function enqueueSessionRun(input: EnqueueSessionRunInput): EnqueueSession
|
|
|
413
430
|
candidate.run.queuedAt = nowMs
|
|
414
431
|
}
|
|
415
432
|
if (input.onEvent) candidate.onEvents.push(input.onEvent)
|
|
433
|
+
if (input.callerSignal) chainCallerSignal(input.callerSignal, candidate.signalController)
|
|
416
434
|
emitRunMeta(candidate, 'queued', { position: 0, coalesced: true, mergedIntoRunId: candidate.run.id })
|
|
417
435
|
return {
|
|
418
436
|
runId: candidate.run.id,
|
|
419
437
|
position: 0,
|
|
420
438
|
coalesced: true,
|
|
421
439
|
promise: candidate.promise,
|
|
440
|
+
abort: () => candidate.signalController.abort(),
|
|
422
441
|
}
|
|
423
442
|
}
|
|
424
443
|
}
|
|
@@ -463,12 +482,14 @@ export function enqueueSessionRun(input: EnqueueSessionRunInput): EnqueueSession
|
|
|
463
482
|
promise,
|
|
464
483
|
}
|
|
465
484
|
|
|
485
|
+
if (input.callerSignal) chainCallerSignal(input.callerSignal, entry.signalController)
|
|
486
|
+
|
|
466
487
|
q.push(entry)
|
|
467
488
|
const position = (running ? 1 : 0) + q.length - 1
|
|
468
489
|
emitRunMeta(entry, 'queued', { position })
|
|
469
490
|
void drainExecution(executionKey)
|
|
470
491
|
|
|
471
|
-
return { runId, position, promise }
|
|
492
|
+
return { runId, position, promise, abort: () => entry.signalController.abort() }
|
|
472
493
|
}
|
|
473
494
|
|
|
474
495
|
export function getSessionRunState(sessionId: string): {
|