@swarmclawai/swarmclaw 0.7.7 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -14
- package/next.config.ts +13 -2
- package/package.json +4 -2
- package/src/app/api/agents/[id]/thread/route.ts +9 -0
- package/src/app/api/agents/route.ts +4 -0
- package/src/app/api/agents/thread-route.test.ts +133 -0
- package/src/app/api/approvals/route.test.ts +148 -0
- package/src/app/api/canvas/[sessionId]/route.ts +3 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
- package/src/app/api/chats/[id]/devserver/route.ts +48 -7
- package/src/app/api/chats/[id]/messages/route.ts +42 -18
- package/src/app/api/chats/[id]/route.ts +1 -1
- package/src/app/api/chats/[id]/stop/route.ts +5 -4
- package/src/app/api/chats/route.ts +23 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +46 -3
- package/src/app/api/connectors/route.ts +12 -8
- package/src/app/api/external-agents/route.test.ts +165 -0
- package/src/app/api/gateways/[id]/health/route.ts +27 -12
- package/src/app/api/gateways/[id]/route.ts +2 -0
- package/src/app/api/gateways/health-route.test.ts +135 -0
- package/src/app/api/gateways/route.ts +2 -0
- package/src/app/api/mcp-servers/route.test.ts +130 -0
- package/src/app/api/openclaw/deploy/route.ts +38 -5
- package/src/app/api/plugins/install/route.ts +46 -6
- package/src/app/api/plugins/marketplace/route.ts +48 -15
- package/src/app/api/preview-server/route.ts +26 -11
- package/src/app/api/projects/[id]/route.ts +6 -2
- package/src/app/api/projects/route.ts +4 -3
- package/src/app/api/schedules/[id]/run/route.ts +4 -0
- package/src/app/api/schedules/route.test.ts +86 -0
- package/src/app/api/schedules/route.ts +6 -1
- 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/app/api/setup/check-provider/route.test.ts +19 -0
- package/src/app/api/setup/check-provider/route.ts +40 -10
- package/src/app/api/skills/[id]/route.ts +12 -0
- package/src/app/api/skills/import/route.ts +14 -12
- package/src/app/api/skills/route.ts +13 -1
- package/src/app/api/tasks/[id]/route.ts +10 -1
- package/src/app/api/tasks/import/github/route.test.ts +65 -0
- package/src/app/api/tasks/import/github/route.ts +337 -0
- package/src/app/api/wallets/[id]/approve/route.ts +17 -3
- package/src/app/api/wallets/[id]/route.ts +79 -33
- package/src/app/api/wallets/[id]/send/route.ts +19 -33
- package/src/app/api/wallets/route.ts +78 -61
- package/src/app/api/webhooks/[id]/route.ts +33 -6
- package/src/app/api/webhooks/route.test.ts +272 -0
- package/src/cli/index.js +1 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-card.tsx +9 -2
- package/src/components/agents/agent-chat-list.tsx +18 -2
- package/src/components/agents/agent-list.tsx +1 -0
- package/src/components/agents/agent-sheet.tsx +257 -38
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-area.tsx +36 -19
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +48 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- package/src/components/chat/delegation-banner.test.ts +14 -1
- package/src/components/chat/delegation-banner.tsx +1 -1
- package/src/components/chat/message-bubble.tsx +208 -145
- package/src/components/chat/message-list.tsx +48 -19
- package/src/components/chatrooms/chatroom-message.tsx +2 -2
- package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
- package/src/components/connectors/connector-health.tsx +1 -1
- package/src/components/connectors/connector-list.tsx +7 -2
- package/src/components/connectors/connector-sheet.tsx +337 -148
- package/src/components/gateways/gateway-sheet.tsx +2 -2
- package/src/components/layout/app-layout.tsx +40 -23
- package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
- package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
- package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
- package/src/components/plugins/plugin-list.tsx +45 -9
- package/src/components/plugins/plugin-sheet.tsx +55 -7
- 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 +2 -1
- package/src/components/providers/provider-sheet.tsx +21 -2
- package/src/components/schedules/schedule-card.tsx +25 -1
- package/src/components/schedules/schedule-sheet.tsx +44 -2
- package/src/components/secrets/secret-sheet.tsx +21 -2
- package/src/components/shared/agent-switch-dialog.tsx +12 -1
- package/src/components/shared/bottom-sheet.tsx +13 -3
- package/src/components/shared/command-palette.tsx +8 -1
- package/src/components/shared/confirm-dialog.tsx +19 -4
- package/src/components/shared/connector-platform-icon.test.ts +28 -0
- package/src/components/shared/connector-platform-icon.tsx +39 -6
- package/src/components/shared/settings/plugin-manager.tsx +29 -6
- package/src/components/shared/settings/section-capability-policy.tsx +45 -3
- package/src/components/shared/settings/section-voice.tsx +11 -3
- package/src/components/skills/skill-list.tsx +25 -0
- package/src/components/skills/skill-sheet.tsx +84 -12
- package/src/components/tasks/approvals-panel.tsx +289 -34
- package/src/components/tasks/task-board.tsx +410 -25
- package/src/components/tasks/task-card.tsx +66 -8
- package/src/components/tasks/task-sheet.tsx +16 -4
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
- package/src/components/wallets/wallet-panel.tsx +435 -90
- package/src/components/wallets/wallet-section.tsx +198 -48
- package/src/components/webhooks/webhook-sheet.tsx +22 -2
- package/src/lib/approval-display.ts +20 -0
- package/src/lib/canvas-content.ts +198 -0
- package/src/lib/chat-artifact-summary.ts +165 -0
- package/src/lib/chat-display.test.ts +91 -0
- package/src/lib/chat-display.ts +58 -0
- package/src/lib/chat-streaming-state.test.ts +47 -1
- package/src/lib/chat-streaming-state.ts +42 -0
- package/src/lib/ollama-model.ts +10 -0
- package/src/lib/openclaw-endpoint.test.ts +8 -0
- package/src/lib/openclaw-endpoint.ts +6 -1
- package/src/lib/plugin-install-cors.ts +46 -0
- package/src/lib/plugin-sources.test.ts +43 -0
- package/src/lib/plugin-sources.ts +77 -0
- package/src/lib/providers/ollama.ts +16 -6
- package/src/lib/providers/openclaw.test.ts +54 -0
- package/src/lib/providers/openclaw.ts +127 -11
- package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
- package/src/lib/schedule-dedupe.test.ts +66 -1
- package/src/lib/schedule-dedupe.ts +169 -12
- package/src/lib/schedule-origin.test.ts +20 -0
- package/src/lib/schedule-origin.ts +15 -0
- package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
- package/src/lib/server/agent-availability.ts +16 -0
- package/src/lib/server/agent-runtime-config.ts +12 -4
- package/src/lib/server/agent-thread-session.test.ts +51 -0
- package/src/lib/server/agent-thread-session.ts +7 -0
- package/src/lib/server/approval-match.ts +205 -0
- package/src/lib/server/approvals-auto-approve.test.ts +538 -1
- package/src/lib/server/approvals.ts +214 -1
- package/src/lib/server/assistant-control.test.ts +29 -0
- package/src/lib/server/assistant-control.ts +23 -0
- package/src/lib/server/build-llm.test.ts +79 -0
- package/src/lib/server/build-llm.ts +14 -4
- package/src/lib/server/canvas-content.test.ts +32 -0
- package/src/lib/server/canvas-content.ts +6 -0
- package/src/lib/server/capability-router.test.ts +33 -0
- package/src/lib/server/capability-router.ts +80 -19
- package/src/lib/server/chat-execution-advanced.test.ts +651 -0
- package/src/lib/server/chat-execution-disabled.test.ts +94 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
- package/src/lib/server/chat-execution.ts +378 -73
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +461 -137
- package/src/lib/server/connectors/pairing.ts +26 -5
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp.test.ts +134 -0
- package/src/lib/server/connectors/whatsapp.ts +271 -47
- package/src/lib/server/context-manager.ts +6 -1
- package/src/lib/server/daemon-state.ts +84 -47
- package/src/lib/server/data-dir.test.ts +37 -0
- package/src/lib/server/data-dir.ts +20 -1
- package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
- package/src/lib/server/devserver-launch.test.ts +60 -0
- package/src/lib/server/devserver-launch.ts +85 -0
- package/src/lib/server/elevenlabs.test.ts +247 -1
- package/src/lib/server/elevenlabs.ts +147 -43
- package/src/lib/server/ethereum.ts +590 -0
- package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
- package/src/lib/server/eval/agent-regression.test.ts +18 -1
- package/src/lib/server/eval/agent-regression.ts +383 -11
- package/src/lib/server/evm-swap.ts +475 -0
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
- package/src/lib/server/heartbeat-service.ts +20 -11
- package/src/lib/server/heartbeat-wake.test.ts +112 -0
- package/src/lib/server/heartbeat-wake.ts +338 -57
- package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
- 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/mcp-client.test.ts +16 -0
- package/src/lib/server/mcp-client.ts +25 -0
- package/src/lib/server/memory-integration.test.ts +719 -0
- package/src/lib/server/memory-policy.test.ts +43 -0
- package/src/lib/server/memory-policy.ts +132 -0
- package/src/lib/server/memory-tiers.test.ts +60 -0
- package/src/lib/server/memory-tiers.ts +16 -0
- package/src/lib/server/ollama-runtime.ts +58 -0
- package/src/lib/server/openclaw-deploy.test.ts +109 -1
- package/src/lib/server/openclaw-deploy.ts +557 -81
- package/src/lib/server/openclaw-gateway.test.ts +131 -0
- package/src/lib/server/openclaw-gateway.ts +10 -4
- package/src/lib/server/openclaw-health.test.ts +35 -0
- package/src/lib/server/openclaw-health.ts +215 -47
- package/src/lib/server/orchestrator-lg.ts +3 -2
- package/src/lib/server/orchestrator.ts +2 -0
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +211 -6
- package/src/lib/server/project-context.ts +162 -0
- package/src/lib/server/project-utils.ts +150 -0
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +409 -2
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +527 -68
- package/src/lib/server/scheduler.ts +29 -1
- package/src/lib/server/session-note.test.ts +36 -0
- package/src/lib/server/session-note.ts +42 -0
- package/src/lib/server/session-run-manager.ts +83 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +366 -54
- package/src/lib/server/session-tools/context.ts +17 -3
- package/src/lib/server/session-tools/crud.ts +484 -84
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
- package/src/lib/server/session-tools/delegate.ts +102 -10
- package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
- package/src/lib/server/session-tools/discovery.ts +80 -12
- package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
- package/src/lib/server/session-tools/file.ts +43 -4
- package/src/lib/server/session-tools/human-loop.ts +35 -5
- package/src/lib/server/session-tools/index.ts +44 -9
- package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
- package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
- package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
- package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
- package/src/lib/server/session-tools/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +554 -75
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- 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/plugin-creator.ts +57 -1
- package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
- package/src/lib/server/session-tools/schedule.ts +6 -1
- package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
- package/src/lib/server/session-tools/shell.ts +22 -3
- package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
- package/src/lib/server/session-tools/wallet.ts +1374 -139
- package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
- package/src/lib/server/session-tools/web.ts +621 -70
- package/src/lib/server/skill-discovery.ts +128 -0
- package/src/lib/server/skill-eligibility.test.ts +84 -0
- package/src/lib/server/skill-eligibility.ts +95 -0
- package/src/lib/server/skill-prompt-budget.test.ts +102 -0
- package/src/lib/server/skill-prompt-budget.ts +125 -0
- package/src/lib/server/skills-normalize.test.ts +54 -0
- package/src/lib/server/skills-normalize.ts +372 -26
- package/src/lib/server/solana.ts +214 -29
- package/src/lib/server/storage.ts +65 -36
- package/src/lib/server/stream-agent-chat.test.ts +437 -2
- package/src/lib/server/stream-agent-chat.ts +957 -79
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-aliases.ts +2 -0
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -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-loop-detection.test.ts +105 -0
- package/src/lib/server/tool-loop-detection.ts +260 -0
- package/src/lib/server/tool-planning.test.ts +44 -0
- package/src/lib/server/tool-planning.ts +271 -0
- package/src/lib/server/wallet-execution.test.ts +198 -0
- package/src/lib/server/wallet-portfolio.test.ts +98 -0
- package/src/lib/server/wallet-portfolio.ts +724 -0
- package/src/lib/server/wallet-service.test.ts +57 -0
- package/src/lib/server/wallet-service.ts +213 -0
- package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
- package/src/lib/server/watch-jobs.ts +17 -2
- package/src/lib/server/workspace-context.ts +111 -0
- package/src/lib/skill-save-payload.test.ts +39 -0
- package/src/lib/skill-save-payload.ts +37 -0
- package/src/lib/tasks.ts +28 -0
- package/src/lib/tool-definitions.ts +2 -1
- package/src/lib/tool-event-summary.test.ts +30 -0
- package/src/lib/tool-event-summary.ts +37 -0
- package/src/lib/validation/schemas.ts +1 -0
- package/src/lib/wallet-transactions.test.ts +75 -0
- package/src/lib/wallet-transactions.ts +43 -0
- package/src/lib/wallet.test.ts +17 -0
- package/src/lib/wallet.ts +183 -0
- package/src/proxy.test.ts +31 -0
- package/src/proxy.ts +34 -2
- package/src/stores/use-chat-store.ts +15 -1
- package/src/types/index.ts +249 -14
package/src/lib/server/queue.ts
CHANGED
|
@@ -13,7 +13,9 @@ 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
|
|
16
|
+
import { shouldAutoDeleteScheduleAfterTerminalRun } from '@/lib/schedule-origin'
|
|
17
|
+
import type { Agent, BoardTask, Connector, Message, Session } from '@/types'
|
|
18
|
+
import { buildAgentDisabledMessage, isAgentDisabled } from './agent-availability'
|
|
17
19
|
|
|
18
20
|
// HMR-safe: pin processing flag to globalThis so hot reloads don't reset it
|
|
19
21
|
const _queueState = ((globalThis as Record<string, unknown>).__swarmclaw_queue__ ??= { processing: false, pendingKick: false }) as { processing: boolean; pendingKick: boolean }
|
|
@@ -23,9 +25,11 @@ interface SessionMessageLike {
|
|
|
23
25
|
text?: string
|
|
24
26
|
time?: number
|
|
25
27
|
kind?: string
|
|
28
|
+
historyExcluded?: boolean
|
|
26
29
|
source?: {
|
|
27
30
|
connectorId?: string
|
|
28
31
|
channelId?: string
|
|
32
|
+
threadId?: string
|
|
29
33
|
}
|
|
30
34
|
toolEvents?: Array<{ name?: string; output?: string }>
|
|
31
35
|
streaming?: boolean
|
|
@@ -37,7 +41,17 @@ interface SessionLike {
|
|
|
37
41
|
user?: string
|
|
38
42
|
cwd?: string
|
|
39
43
|
messages?: SessionMessageLike[]
|
|
44
|
+
connectorContext?: {
|
|
45
|
+
connectorId?: string | null
|
|
46
|
+
channelId?: string | null
|
|
47
|
+
threadId?: string | null
|
|
48
|
+
senderId?: string | null
|
|
49
|
+
senderName?: string | null
|
|
50
|
+
}
|
|
40
51
|
lastActiveAt?: number
|
|
52
|
+
heartbeatEnabled?: boolean | null
|
|
53
|
+
active?: boolean
|
|
54
|
+
currentRunId?: string | null
|
|
41
55
|
}
|
|
42
56
|
|
|
43
57
|
interface ScheduleTaskMeta extends BoardTask {
|
|
@@ -58,8 +72,11 @@ interface RunningConnectorLike {
|
|
|
58
72
|
interface ConnectorTaskFollowupTarget {
|
|
59
73
|
connectorId: string
|
|
60
74
|
channelId: string
|
|
75
|
+
threadId?: string | null
|
|
61
76
|
}
|
|
62
77
|
|
|
78
|
+
const DISABLED_AGENT_RETRY_MS = 60_000
|
|
79
|
+
|
|
63
80
|
function sameReasons(a?: string[] | null, b?: string[] | null): boolean {
|
|
64
81
|
const av = Array.isArray(a) ? a : []
|
|
65
82
|
const bv = Array.isArray(b) ? b : []
|
|
@@ -128,6 +145,212 @@ function applyTaskPolicyDefaults(task: BoardTask): void {
|
|
|
128
145
|
if (task.deadLetteredAt === undefined) task.deadLetteredAt = null
|
|
129
146
|
}
|
|
130
147
|
|
|
148
|
+
export interface TaskResumeState {
|
|
149
|
+
claudeSessionId: string | null
|
|
150
|
+
codexThreadId: string | null
|
|
151
|
+
opencodeSessionId: string | null
|
|
152
|
+
delegateResumeIds: NonNullable<Session['delegateResumeIds']>
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface TaskResumeContext {
|
|
156
|
+
source: 'self' | 'delegated_from_task' | 'blocked_by'
|
|
157
|
+
sourceTaskId: string
|
|
158
|
+
sourceTaskTitle: string
|
|
159
|
+
sourceSessionId: string | null
|
|
160
|
+
resume: TaskResumeState
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function normalizeResumeHandle(value: unknown): string | null {
|
|
164
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function buildEmptyDelegateResumeIds(): NonNullable<Session['delegateResumeIds']> {
|
|
168
|
+
return {
|
|
169
|
+
claudeCode: null,
|
|
170
|
+
codex: null,
|
|
171
|
+
opencode: null,
|
|
172
|
+
gemini: null,
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function normalizeCliProvider(value: unknown): string | null {
|
|
177
|
+
return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : null
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function hasResumeState(state: TaskResumeState | null | undefined): state is TaskResumeState {
|
|
181
|
+
if (!state) return false
|
|
182
|
+
return Boolean(
|
|
183
|
+
state.claudeSessionId
|
|
184
|
+
|| state.codexThreadId
|
|
185
|
+
|| state.opencodeSessionId
|
|
186
|
+
|| state.delegateResumeIds.claudeCode
|
|
187
|
+
|| state.delegateResumeIds.codex
|
|
188
|
+
|| state.delegateResumeIds.opencode
|
|
189
|
+
|| state.delegateResumeIds.gemini
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function extractTaskResumeState(task: Partial<BoardTask> | null | undefined): TaskResumeState | null {
|
|
194
|
+
if (!task) return null
|
|
195
|
+
|
|
196
|
+
const legacyResumeId = normalizeResumeHandle(task.cliResumeId)
|
|
197
|
+
const legacyProvider = normalizeCliProvider(task.cliProvider)
|
|
198
|
+
const claudeSessionId = normalizeResumeHandle(task.claudeResumeId)
|
|
199
|
+
|| (legacyProvider === 'claude-cli' ? legacyResumeId : null)
|
|
200
|
+
const codexThreadId = normalizeResumeHandle(task.codexResumeId)
|
|
201
|
+
|| (legacyProvider === 'codex-cli' ? legacyResumeId : null)
|
|
202
|
+
const opencodeSessionId = normalizeResumeHandle(task.opencodeResumeId)
|
|
203
|
+
|| (legacyProvider === 'opencode-cli' ? legacyResumeId : null)
|
|
204
|
+
const geminiSessionId = normalizeResumeHandle(task.geminiResumeId)
|
|
205
|
+
|| (legacyProvider === 'gemini-cli' ? legacyResumeId : null)
|
|
206
|
+
|
|
207
|
+
const resume = {
|
|
208
|
+
claudeSessionId,
|
|
209
|
+
codexThreadId,
|
|
210
|
+
opencodeSessionId,
|
|
211
|
+
delegateResumeIds: {
|
|
212
|
+
claudeCode: claudeSessionId,
|
|
213
|
+
codex: codexThreadId,
|
|
214
|
+
opencode: opencodeSessionId,
|
|
215
|
+
gemini: geminiSessionId,
|
|
216
|
+
},
|
|
217
|
+
} satisfies TaskResumeState
|
|
218
|
+
|
|
219
|
+
return hasResumeState(resume) ? resume : null
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function extractSessionResumeState(session: Partial<Session> | null | undefined): TaskResumeState | null {
|
|
223
|
+
if (!session) return null
|
|
224
|
+
|
|
225
|
+
const claudeSessionId = normalizeResumeHandle(session.claudeSessionId)
|
|
226
|
+
const codexThreadId = normalizeResumeHandle(session.codexThreadId)
|
|
227
|
+
const opencodeSessionId = normalizeResumeHandle(session.opencodeSessionId)
|
|
228
|
+
const delegateResumeIds = session.delegateResumeIds && typeof session.delegateResumeIds === 'object'
|
|
229
|
+
? { ...buildEmptyDelegateResumeIds(), ...session.delegateResumeIds }
|
|
230
|
+
: buildEmptyDelegateResumeIds()
|
|
231
|
+
|
|
232
|
+
const resume = {
|
|
233
|
+
claudeSessionId,
|
|
234
|
+
codexThreadId,
|
|
235
|
+
opencodeSessionId,
|
|
236
|
+
delegateResumeIds: {
|
|
237
|
+
claudeCode: normalizeResumeHandle(delegateResumeIds.claudeCode) || claudeSessionId,
|
|
238
|
+
codex: normalizeResumeHandle(delegateResumeIds.codex) || codexThreadId,
|
|
239
|
+
opencode: normalizeResumeHandle(delegateResumeIds.opencode) || opencodeSessionId,
|
|
240
|
+
gemini: normalizeResumeHandle(delegateResumeIds.gemini),
|
|
241
|
+
},
|
|
242
|
+
} satisfies TaskResumeState
|
|
243
|
+
|
|
244
|
+
return hasResumeState(resume) ? resume : null
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function resolveTaskResumeContext(
|
|
248
|
+
task: BoardTask,
|
|
249
|
+
tasksById: Record<string, BoardTask>,
|
|
250
|
+
sessionsById?: Record<string, SessionLike | Session>,
|
|
251
|
+
): TaskResumeContext | null {
|
|
252
|
+
const candidates: Array<{ source: TaskResumeContext['source']; taskId: string | null | undefined }> = [
|
|
253
|
+
{ source: 'self', taskId: task.id },
|
|
254
|
+
{ source: 'delegated_from_task', taskId: task.delegatedFromTaskId },
|
|
255
|
+
...((Array.isArray(task.blockedBy) ? task.blockedBy : []).map((taskId) => ({ source: 'blocked_by' as const, taskId }))),
|
|
256
|
+
]
|
|
257
|
+
const seen = new Set<string>()
|
|
258
|
+
|
|
259
|
+
for (const candidate of candidates) {
|
|
260
|
+
const taskId = typeof candidate.taskId === 'string' ? candidate.taskId.trim() : ''
|
|
261
|
+
if (!taskId || seen.has(taskId)) continue
|
|
262
|
+
seen.add(taskId)
|
|
263
|
+
const sourceTask = taskId === task.id ? task : tasksById[taskId]
|
|
264
|
+
if (!sourceTask) continue
|
|
265
|
+
const sourceSessionId = normalizeResumeHandle(sourceTask.checkpoint?.lastSessionId) || normalizeResumeHandle(sourceTask.sessionId)
|
|
266
|
+
const resume = extractTaskResumeState(sourceTask)
|
|
267
|
+
|| (sourceSessionId && sessionsById?.[sourceSessionId]
|
|
268
|
+
? extractSessionResumeState(sessionsById[sourceSessionId] as Session)
|
|
269
|
+
: null)
|
|
270
|
+
if (!resume) continue
|
|
271
|
+
return {
|
|
272
|
+
source: candidate.source,
|
|
273
|
+
sourceTaskId: sourceTask.id,
|
|
274
|
+
sourceTaskTitle: sourceTask.title,
|
|
275
|
+
sourceSessionId,
|
|
276
|
+
resume,
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return null
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function applyTaskResumeStateToSession(session: Session, resume: TaskResumeState | null | undefined): boolean {
|
|
284
|
+
if (!hasResumeState(resume)) return false
|
|
285
|
+
|
|
286
|
+
let changed = false
|
|
287
|
+
const directFields: Array<['claudeSessionId' | 'codexThreadId' | 'opencodeSessionId', string | null]> = [
|
|
288
|
+
['claudeSessionId', resume.claudeSessionId],
|
|
289
|
+
['codexThreadId', resume.codexThreadId],
|
|
290
|
+
['opencodeSessionId', resume.opencodeSessionId],
|
|
291
|
+
]
|
|
292
|
+
for (const [key, value] of directFields) {
|
|
293
|
+
if (!value || session[key] === value) continue
|
|
294
|
+
session[key] = value
|
|
295
|
+
changed = true
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const currentDelegateResume = session.delegateResumeIds && typeof session.delegateResumeIds === 'object'
|
|
299
|
+
? { ...buildEmptyDelegateResumeIds(), ...session.delegateResumeIds }
|
|
300
|
+
: buildEmptyDelegateResumeIds()
|
|
301
|
+
for (const [key, value] of Object.entries(resume.delegateResumeIds) as Array<[keyof NonNullable<Session['delegateResumeIds']>, string | null]>) {
|
|
302
|
+
if (!value || currentDelegateResume[key] === value) continue
|
|
303
|
+
currentDelegateResume[key] = value
|
|
304
|
+
changed = true
|
|
305
|
+
}
|
|
306
|
+
if (changed) session.delegateResumeIds = currentDelegateResume
|
|
307
|
+
return changed
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function resolveReusableTaskSessionId(
|
|
311
|
+
task: BoardTask,
|
|
312
|
+
tasks: Record<string, BoardTask>,
|
|
313
|
+
sessions: Record<string, SessionLike>,
|
|
314
|
+
): string {
|
|
315
|
+
const candidateTaskIds = [
|
|
316
|
+
task.id,
|
|
317
|
+
typeof task.delegatedFromTaskId === 'string' ? task.delegatedFromTaskId : '',
|
|
318
|
+
...(Array.isArray(task.blockedBy) ? task.blockedBy : []),
|
|
319
|
+
]
|
|
320
|
+
const seen = new Set<string>()
|
|
321
|
+
for (const candidateTaskId of candidateTaskIds) {
|
|
322
|
+
const taskId = typeof candidateTaskId === 'string' ? candidateTaskId.trim() : ''
|
|
323
|
+
if (!taskId || seen.has(taskId)) continue
|
|
324
|
+
seen.add(taskId)
|
|
325
|
+
const sourceTask = taskId === task.id ? task : tasks[taskId]
|
|
326
|
+
if (!sourceTask) continue
|
|
327
|
+
const candidates = [
|
|
328
|
+
normalizeResumeHandle(sourceTask.checkpoint?.lastSessionId),
|
|
329
|
+
normalizeResumeHandle(sourceTask.sessionId),
|
|
330
|
+
]
|
|
331
|
+
for (const candidate of candidates) {
|
|
332
|
+
if (candidate && sessions[candidate]) return candidate
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return ''
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function buildTaskContinuationNote(
|
|
339
|
+
reusedExistingSession: boolean,
|
|
340
|
+
resumeContext: TaskResumeContext | null,
|
|
341
|
+
): string {
|
|
342
|
+
const notes: string[] = []
|
|
343
|
+
if (reusedExistingSession) {
|
|
344
|
+
notes.push('Reusing the previous execution session for this task.')
|
|
345
|
+
}
|
|
346
|
+
if (resumeContext?.source === 'delegated_from_task' || resumeContext?.source === 'blocked_by') {
|
|
347
|
+
notes.push(`Stored CLI context is available from related task "${resumeContext.sourceTaskTitle}".`)
|
|
348
|
+
} else if (resumeContext?.source === 'self' && !reusedExistingSession) {
|
|
349
|
+
notes.push('Stored CLI resume handles are available for continuation.')
|
|
350
|
+
}
|
|
351
|
+
return notes.length ? `\n\n${notes.join(' ')}` : ''
|
|
352
|
+
}
|
|
353
|
+
|
|
131
354
|
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
|
|
132
355
|
const TASK_CWD_NOISE_DIRS = new Set([
|
|
133
356
|
'uploads',
|
|
@@ -460,12 +683,10 @@ export function resolveTaskOriginConnectorFollowupTarget(params: {
|
|
|
460
683
|
const delegatedByAgentId = typeof metaTask.delegatedByAgentId === 'string'
|
|
461
684
|
? metaTask.delegatedByAgentId.trim()
|
|
462
685
|
: ''
|
|
686
|
+
const allowedOwners = new Set([task.agentId, delegatedByAgentId].filter(Boolean))
|
|
463
687
|
const sourceSessionId = typeof metaTask.createdInSessionId === 'string'
|
|
464
688
|
? metaTask.createdInSessionId.trim()
|
|
465
689
|
: ''
|
|
466
|
-
if (!sourceSessionId) return null
|
|
467
|
-
const sourceSession = sessions[sourceSessionId]
|
|
468
|
-
if (!sourceSession || !Array.isArray(sourceSession.messages)) return null
|
|
469
690
|
|
|
470
691
|
const runningById = new Map<string, RunningConnectorLike>()
|
|
471
692
|
for (const entry of running) {
|
|
@@ -473,9 +694,64 @@ export function resolveTaskOriginConnectorFollowupTarget(params: {
|
|
|
473
694
|
runningById.set(entry.id, entry)
|
|
474
695
|
}
|
|
475
696
|
|
|
697
|
+
const normalizeTarget = (raw: {
|
|
698
|
+
connectorId?: string | null
|
|
699
|
+
channelId?: string | null
|
|
700
|
+
threadId?: string | null
|
|
701
|
+
}): ConnectorTaskFollowupTarget | null => {
|
|
702
|
+
const connectorId = typeof raw.connectorId === 'string' ? raw.connectorId.trim() : ''
|
|
703
|
+
if (!connectorId) return null
|
|
704
|
+
const connector = connectors[connectorId]
|
|
705
|
+
if (!connector) return null
|
|
706
|
+
const ownerId = typeof connector.agentId === 'string' ? connector.agentId.trim() : ''
|
|
707
|
+
if (ownerId && !allowedOwners.has(ownerId)) return null
|
|
708
|
+
|
|
709
|
+
const runtime = runningById.get(connectorId)
|
|
710
|
+
if (runtime && !runtime.supportsSend) return null
|
|
711
|
+
|
|
712
|
+
const channelId = typeof raw.channelId === 'string' ? raw.channelId.trim() : ''
|
|
713
|
+
if (!channelId) return null
|
|
714
|
+
const normalizedChannelId = connector.platform === 'whatsapp'
|
|
715
|
+
? normalizeWhatsappTarget(channelId)
|
|
716
|
+
: channelId
|
|
717
|
+
const threadId = typeof raw.threadId === 'string' ? raw.threadId.trim() : ''
|
|
718
|
+
return {
|
|
719
|
+
connectorId,
|
|
720
|
+
channelId: normalizedChannelId,
|
|
721
|
+
...(threadId ? { threadId } : {}),
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const explicitTarget = normalizeTarget({
|
|
726
|
+
connectorId: typeof metaTask.followupConnectorId === 'string' ? metaTask.followupConnectorId : null,
|
|
727
|
+
channelId: typeof metaTask.followupChannelId === 'string' ? metaTask.followupChannelId : null,
|
|
728
|
+
threadId: typeof metaTask.followupThreadId === 'string' ? metaTask.followupThreadId : null,
|
|
729
|
+
})
|
|
730
|
+
if (explicitTarget) return explicitTarget
|
|
731
|
+
|
|
732
|
+
if (!sourceSessionId) return null
|
|
733
|
+
const sourceSession = sessions[sourceSessionId]
|
|
734
|
+
if (!sourceSession) return null
|
|
735
|
+
|
|
736
|
+
const sessionContextTarget = normalizeTarget({
|
|
737
|
+
connectorId: typeof sourceSession.connectorContext?.connectorId === 'string'
|
|
738
|
+
? sourceSession.connectorContext.connectorId
|
|
739
|
+
: null,
|
|
740
|
+
channelId: typeof sourceSession.connectorContext?.channelId === 'string'
|
|
741
|
+
? sourceSession.connectorContext.channelId
|
|
742
|
+
: null,
|
|
743
|
+
threadId: typeof sourceSession.connectorContext?.threadId === 'string'
|
|
744
|
+
? sourceSession.connectorContext.threadId
|
|
745
|
+
: null,
|
|
746
|
+
})
|
|
747
|
+
if (sessionContextTarget) return sessionContextTarget
|
|
748
|
+
|
|
749
|
+
if (!Array.isArray(sourceSession.messages)) return null
|
|
750
|
+
|
|
476
751
|
for (let i = sourceSession.messages.length - 1; i >= 0; i--) {
|
|
477
752
|
const message = sourceSession.messages[i]
|
|
478
753
|
if (!message || message.role !== 'user') continue
|
|
754
|
+
if (message.historyExcluded === true) continue
|
|
479
755
|
|
|
480
756
|
const connectorId = typeof message.source?.connectorId === 'string'
|
|
481
757
|
? message.source.connectorId.trim()
|
|
@@ -484,15 +760,7 @@ export function resolveTaskOriginConnectorFollowupTarget(params: {
|
|
|
484
760
|
|
|
485
761
|
const connector = connectors[connectorId]
|
|
486
762
|
if (!connector) continue
|
|
487
|
-
const ownerId = typeof connector.agentId === 'string' ? connector.agentId.trim() : ''
|
|
488
|
-
if (ownerId) {
|
|
489
|
-
const allowedOwners = new Set([task.agentId, delegatedByAgentId].filter(Boolean))
|
|
490
|
-
if (!allowedOwners.has(ownerId)) continue
|
|
491
|
-
}
|
|
492
|
-
|
|
493
763
|
const runtime = runningById.get(connectorId)
|
|
494
|
-
if (runtime && !runtime.supportsSend) continue
|
|
495
|
-
|
|
496
764
|
const sourceChannel = typeof message.source?.channelId === 'string'
|
|
497
765
|
? message.source.channelId.trim()
|
|
498
766
|
: ''
|
|
@@ -501,20 +769,59 @@ export function resolveTaskOriginConnectorFollowupTarget(params: {
|
|
|
501
769
|
|| connector.config?.outboundJid
|
|
502
770
|
|| connector.config?.outboundTarget
|
|
503
771
|
|| ''
|
|
504
|
-
const
|
|
505
|
-
if (!rawChannel) continue
|
|
506
|
-
|
|
507
|
-
return {
|
|
772
|
+
const target = normalizeTarget({
|
|
508
773
|
connectorId,
|
|
509
|
-
channelId:
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
774
|
+
channelId: sourceChannel || fallbackChannel,
|
|
775
|
+
threadId: typeof message.source?.threadId === 'string' ? message.source.threadId : null,
|
|
776
|
+
})
|
|
777
|
+
if (target) return target
|
|
513
778
|
}
|
|
514
779
|
|
|
515
780
|
return null
|
|
516
781
|
}
|
|
517
782
|
|
|
783
|
+
export function collectTaskConnectorFollowupTargets(params: {
|
|
784
|
+
task: BoardTask
|
|
785
|
+
sessions: Record<string, SessionLike>
|
|
786
|
+
connectors: Record<string, Connector>
|
|
787
|
+
running: RunningConnectorLike[]
|
|
788
|
+
}): ConnectorTaskFollowupTarget[] {
|
|
789
|
+
const { task, sessions, connectors, running } = params
|
|
790
|
+
const originTarget = resolveTaskOriginConnectorFollowupTarget({ task, sessions, connectors, running })
|
|
791
|
+
if (originTarget) return [originTarget]
|
|
792
|
+
|
|
793
|
+
const targets: ConnectorTaskFollowupTarget[] = []
|
|
794
|
+
const seen = new Set<string>()
|
|
795
|
+
const pushTarget = (target: ConnectorTaskFollowupTarget | null | undefined) => {
|
|
796
|
+
if (!target?.connectorId || !target?.channelId) return
|
|
797
|
+
const key = `${target.connectorId}|${target.channelId}|${target.threadId || ''}`
|
|
798
|
+
if (seen.has(key)) return
|
|
799
|
+
seen.add(key)
|
|
800
|
+
targets.push(target)
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
for (const entry of running) {
|
|
804
|
+
if (!entry.supportsSend || !entry.id) continue
|
|
805
|
+
const connector = connectors[entry.id]
|
|
806
|
+
if (!connector) continue
|
|
807
|
+
if (connector.agentId !== task.agentId) continue
|
|
808
|
+
if (!isEnabledFlag(connector.config?.taskFollowups)) continue
|
|
809
|
+
const channelTargetRaw = entry.configuredTargets[0]
|
|
810
|
+
|| connector.config?.outboundJid
|
|
811
|
+
|| connector.config?.outboundTarget
|
|
812
|
+
|| ''
|
|
813
|
+
if (!channelTargetRaw) continue
|
|
814
|
+
pushTarget({
|
|
815
|
+
connectorId: entry.id,
|
|
816
|
+
channelId: connector.platform === 'whatsapp'
|
|
817
|
+
? normalizeWhatsappTarget(channelTargetRaw)
|
|
818
|
+
: channelTargetRaw,
|
|
819
|
+
})
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
return targets
|
|
823
|
+
}
|
|
824
|
+
|
|
518
825
|
// Task result extraction now uses Zod-validated structured data
|
|
519
826
|
// from ./task-result.ts (extractTaskResult, formatResultBody)
|
|
520
827
|
|
|
@@ -573,6 +880,132 @@ async function executeTaskRun(
|
|
|
573
880
|
return text
|
|
574
881
|
}
|
|
575
882
|
|
|
883
|
+
function hasFinishedExecutionSession(session: SessionLike | Session | null | undefined): boolean {
|
|
884
|
+
if (!session) return false
|
|
885
|
+
return session.active === false && !session.currentRunId
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
export function reconcileFinishedRunningTasks(): { reconciled: number; deadLettered: number } {
|
|
889
|
+
const tasks = loadTasks()
|
|
890
|
+
const sessions = loadSessions() as Record<string, SessionLike>
|
|
891
|
+
const settings = loadSettings()
|
|
892
|
+
const queue = loadQueue()
|
|
893
|
+
const now = Date.now()
|
|
894
|
+
let reconciled = 0
|
|
895
|
+
let deadLettered = 0
|
|
896
|
+
let tasksDirty = false
|
|
897
|
+
let sessionsDirty = false
|
|
898
|
+
let queueDirty = false
|
|
899
|
+
const terminalTasks: BoardTask[] = []
|
|
900
|
+
|
|
901
|
+
for (const task of Object.values(tasks) as BoardTask[]) {
|
|
902
|
+
if (task.status !== 'running') continue
|
|
903
|
+
const sessionId = typeof task.sessionId === 'string' ? task.sessionId : ''
|
|
904
|
+
if (!sessionId) continue
|
|
905
|
+
const session = sessions[sessionId]
|
|
906
|
+
if (!hasFinishedExecutionSession(session)) continue
|
|
907
|
+
|
|
908
|
+
const fallbackText = latestAssistantText(session)
|
|
909
|
+
if (!fallbackText && !task.result) continue
|
|
910
|
+
|
|
911
|
+
applyTaskPolicyDefaults(task)
|
|
912
|
+
const taskResult = extractTaskResult(
|
|
913
|
+
session,
|
|
914
|
+
task.result || fallbackText || null,
|
|
915
|
+
{ sinceTime: typeof task.startedAt === 'number' ? task.startedAt : null },
|
|
916
|
+
)
|
|
917
|
+
const enrichedResult = formatResultBody(taskResult)
|
|
918
|
+
task.result = enrichedResult.slice(0, 4000) || null
|
|
919
|
+
task.artifacts = taskResult.artifacts.slice(0, 24)
|
|
920
|
+
task.outputFiles = extractLikelyOutputFiles(enrichedResult).slice(0, 24)
|
|
921
|
+
task.updatedAt = now
|
|
922
|
+
const report = ensureTaskCompletionReport(task)
|
|
923
|
+
if (report?.relativePath) task.completionReportPath = report.relativePath
|
|
924
|
+
const validation = validateTaskCompletion(task, { report, settings })
|
|
925
|
+
task.validation = validation
|
|
926
|
+
if (!task.comments) task.comments = []
|
|
927
|
+
|
|
928
|
+
if (validation.ok) {
|
|
929
|
+
task.status = 'completed'
|
|
930
|
+
task.completedAt = now
|
|
931
|
+
task.retryScheduledAt = null
|
|
932
|
+
task.deadLetteredAt = null
|
|
933
|
+
task.error = null
|
|
934
|
+
task.checkpoint = {
|
|
935
|
+
...(task.checkpoint || {}),
|
|
936
|
+
lastRunId: sessionId,
|
|
937
|
+
lastSessionId: sessionId,
|
|
938
|
+
note: 'Recovered completed task state from finished session.',
|
|
939
|
+
updatedAt: now,
|
|
940
|
+
}
|
|
941
|
+
task.comments.push({
|
|
942
|
+
id: genId(),
|
|
943
|
+
author: 'System',
|
|
944
|
+
text: 'Recovered completed task state from a finished execution session.',
|
|
945
|
+
createdAt: now,
|
|
946
|
+
})
|
|
947
|
+
reconciled++
|
|
948
|
+
terminalTasks.push(task)
|
|
949
|
+
} else {
|
|
950
|
+
const failureReason = formatValidationFailure(validation.reasons).slice(0, 500)
|
|
951
|
+
const retryState = scheduleRetryOrDeadLetter(task, failureReason)
|
|
952
|
+
task.completedAt = retryState === 'dead_lettered' ? null : task.completedAt
|
|
953
|
+
task.comments.push({
|
|
954
|
+
id: genId(),
|
|
955
|
+
author: 'System',
|
|
956
|
+
text: `Recovered finished session but the task result failed validation.\n\n${validation.reasons.map((reason) => `- ${reason}`).join('\n')}`,
|
|
957
|
+
createdAt: now,
|
|
958
|
+
})
|
|
959
|
+
if (retryState === 'retry') {
|
|
960
|
+
pushQueueUnique(queue, task.id)
|
|
961
|
+
queueDirty = true
|
|
962
|
+
reconciled++
|
|
963
|
+
pushMainLoopEventToMainSessions({
|
|
964
|
+
type: 'task_retry_scheduled',
|
|
965
|
+
text: `Task retry scheduled: "${task.title}" (${task.id}) attempt ${task.attempts}/${task.maxAttempts} in ${task.retryBackoffSec}s.`,
|
|
966
|
+
})
|
|
967
|
+
} else {
|
|
968
|
+
deadLettered++
|
|
969
|
+
terminalTasks.push(task)
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (session.heartbeatEnabled !== false) {
|
|
974
|
+
session.heartbeatEnabled = false
|
|
975
|
+
session.lastActiveAt = now
|
|
976
|
+
sessionsDirty = true
|
|
977
|
+
}
|
|
978
|
+
tasksDirty = true
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
if (tasksDirty) {
|
|
982
|
+
saveTasks(tasks)
|
|
983
|
+
notify('tasks')
|
|
984
|
+
notify('runs')
|
|
985
|
+
}
|
|
986
|
+
if (sessionsDirty) saveSessions(sessions as Record<string, Session>)
|
|
987
|
+
if (queueDirty) saveQueue(queue)
|
|
988
|
+
|
|
989
|
+
for (const task of terminalTasks) {
|
|
990
|
+
if (task.status === 'completed') {
|
|
991
|
+
pushMainLoopEventToMainSessions({
|
|
992
|
+
type: 'task_completed',
|
|
993
|
+
text: `Task completed: "${task.title}" (${task.id})`,
|
|
994
|
+
})
|
|
995
|
+
} else if (task.status === 'failed') {
|
|
996
|
+
pushMainLoopEventToMainSessions({
|
|
997
|
+
type: 'task_failed',
|
|
998
|
+
text: `Task failed validation: "${task.title}" (${task.id})`,
|
|
999
|
+
})
|
|
1000
|
+
}
|
|
1001
|
+
notifyMainChatScheduleResult(task)
|
|
1002
|
+
notifyAgentThreadTaskResult(task)
|
|
1003
|
+
cleanupTerminalOneOffSchedule(task)
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
return { reconciled, deadLettered }
|
|
1007
|
+
}
|
|
1008
|
+
|
|
576
1009
|
function notifyMainChatScheduleResult(task: BoardTask): void {
|
|
577
1010
|
const scheduleTask = task as ScheduleTaskMeta
|
|
578
1011
|
const sourceType = typeof scheduleTask.sourceType === 'string' ? scheduleTask.sourceType : ''
|
|
@@ -638,6 +1071,22 @@ function notifyMainChatScheduleResult(task: BoardTask): void {
|
|
|
638
1071
|
if (changed) saveSessions(sessions)
|
|
639
1072
|
}
|
|
640
1073
|
|
|
1074
|
+
function cleanupTerminalOneOffSchedule(task: BoardTask): void {
|
|
1075
|
+
const scheduleTask = task as ScheduleTaskMeta
|
|
1076
|
+
const sourceType = typeof scheduleTask.sourceType === 'string' ? scheduleTask.sourceType : ''
|
|
1077
|
+
if (sourceType !== 'schedule') return
|
|
1078
|
+
const scheduleId = typeof scheduleTask.sourceScheduleId === 'string' ? scheduleTask.sourceScheduleId : ''
|
|
1079
|
+
if (!scheduleId) return
|
|
1080
|
+
|
|
1081
|
+
const schedules = loadSchedules()
|
|
1082
|
+
const schedule = schedules[scheduleId]
|
|
1083
|
+
if (!shouldAutoDeleteScheduleAfterTerminalRun(schedule)) return
|
|
1084
|
+
|
|
1085
|
+
delete schedules[scheduleId]
|
|
1086
|
+
saveSchedules(schedules)
|
|
1087
|
+
notify('schedules')
|
|
1088
|
+
}
|
|
1089
|
+
|
|
641
1090
|
async function notifyConnectorTaskFollowups(params: {
|
|
642
1091
|
task: BoardTask
|
|
643
1092
|
statusLabel: string
|
|
@@ -652,54 +1101,23 @@ async function notifyConnectorTaskFollowups(params: {
|
|
|
652
1101
|
const running = (await import('./connectors/manager')).listRunningConnectors()
|
|
653
1102
|
const manager = await import('./connectors/manager')
|
|
654
1103
|
const sessions = loadSessions()
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
1104
|
+
const targets = collectTaskConnectorFollowupTargets({
|
|
1105
|
+
task,
|
|
1106
|
+
sessions: sessions as Record<string, SessionLike>,
|
|
1107
|
+
connectors,
|
|
1108
|
+
running: running as RunningConnectorLike[],
|
|
1109
|
+
})
|
|
1110
|
+
if (!targets.length) return
|
|
663
1111
|
const originTarget = resolveTaskOriginConnectorFollowupTarget({
|
|
664
1112
|
task,
|
|
665
1113
|
sessions: sessions as Record<string, SessionLike>,
|
|
666
1114
|
connectors,
|
|
667
1115
|
running: running as RunningConnectorLike[],
|
|
668
1116
|
})
|
|
669
|
-
addCandidate(originTarget)
|
|
670
1117
|
const preferredTargetKey = originTarget
|
|
671
|
-
? `${originTarget.connectorId}|${originTarget.channelId}`
|
|
1118
|
+
? `${originTarget.connectorId}|${originTarget.channelId}|${originTarget.threadId || ''}`
|
|
672
1119
|
: ''
|
|
673
1120
|
|
|
674
|
-
for (const entry of running) {
|
|
675
|
-
if (!entry.supportsSend || !entry.id) continue
|
|
676
|
-
const connector = connectors[entry.id]
|
|
677
|
-
if (!connector) continue
|
|
678
|
-
if (connector.agentId !== task.agentId) continue
|
|
679
|
-
if (!isEnabledFlag(connector.config?.taskFollowups)) continue
|
|
680
|
-
const channelTargetRaw = entry.recentChannelId
|
|
681
|
-
|| entry.configuredTargets[0]
|
|
682
|
-
|| connector.config?.outboundJid
|
|
683
|
-
|| connector.config?.outboundTarget
|
|
684
|
-
|| ''
|
|
685
|
-
if (!channelTargetRaw) continue
|
|
686
|
-
addCandidate({
|
|
687
|
-
connectorId: entry.id,
|
|
688
|
-
channelId: connector.platform === 'whatsapp'
|
|
689
|
-
? normalizeWhatsappTarget(channelTargetRaw)
|
|
690
|
-
: channelTargetRaw,
|
|
691
|
-
})
|
|
692
|
-
}
|
|
693
|
-
const targets = [...candidateByKey.values()].sort((a, b) => {
|
|
694
|
-
if (!preferredTargetKey) return 0
|
|
695
|
-
const aKey = `${a.connectorId}|${a.channelId}`
|
|
696
|
-
const bKey = `${b.connectorId}|${b.channelId}`
|
|
697
|
-
if (aKey === preferredTargetKey && bKey !== preferredTargetKey) return -1
|
|
698
|
-
if (bKey === preferredTargetKey && aKey !== preferredTargetKey) return 1
|
|
699
|
-
return 0
|
|
700
|
-
})
|
|
701
|
-
if (!targets.length) return
|
|
702
|
-
|
|
703
1121
|
const summary = summaryText.trim().slice(0, 1400)
|
|
704
1122
|
for (const target of targets) {
|
|
705
1123
|
const connector = connectors[target.connectorId]
|
|
@@ -719,7 +1137,7 @@ async function notifyConnectorTaskFollowups(params: {
|
|
|
719
1137
|
`Task ${statusLabel}: ${task.title}`,
|
|
720
1138
|
summary || 'No summary provided.',
|
|
721
1139
|
].join('\n\n')
|
|
722
|
-
const targetKey = `${target.connectorId}|${target.channelId}`
|
|
1140
|
+
const targetKey = `${target.connectorId}|${target.channelId}|${target.threadId || ''}`
|
|
723
1141
|
const preferredChannelNote = !template && preferredTargetKey && targetKey === preferredTargetKey
|
|
724
1142
|
? '\n\n(Update sent in the same channel that requested this task.)'
|
|
725
1143
|
: ''
|
|
@@ -730,6 +1148,7 @@ async function notifyConnectorTaskFollowups(params: {
|
|
|
730
1148
|
await manager.sendConnectorMessage({
|
|
731
1149
|
connectorId: target.connectorId,
|
|
732
1150
|
channelId: target.channelId,
|
|
1151
|
+
threadId: target.threadId || undefined,
|
|
733
1152
|
text: outboundMessage,
|
|
734
1153
|
...(resolvedMediaPath
|
|
735
1154
|
? {
|
|
@@ -1057,7 +1476,7 @@ function scheduleRetryOrDeadLetter(task: BoardTask, reason: string): 'retry' | '
|
|
|
1057
1476
|
return 'dead_lettered'
|
|
1058
1477
|
}
|
|
1059
1478
|
|
|
1060
|
-
function dequeueNextRunnableTask(queue: string[], tasks: Record<string, BoardTask>): string | null {
|
|
1479
|
+
export function dequeueNextRunnableTask(queue: string[], tasks: Record<string, BoardTask>): string | null {
|
|
1061
1480
|
const now = Date.now()
|
|
1062
1481
|
|
|
1063
1482
|
// Remove stale entries first.
|
|
@@ -1071,7 +1490,9 @@ function dequeueNextRunnableTask(queue: string[], tasks: Record<string, BoardTas
|
|
|
1071
1490
|
const task = tasks[id]
|
|
1072
1491
|
if (!task) return false
|
|
1073
1492
|
const retryAt = typeof task.retryScheduledAt === 'number' ? task.retryScheduledAt : null
|
|
1074
|
-
|
|
1493
|
+
if (retryAt && retryAt > now) return false
|
|
1494
|
+
const blockers = Array.isArray(task.blockedBy) ? task.blockedBy : []
|
|
1495
|
+
return blockers.every((blockerId) => tasks[blockerId]?.status === 'completed')
|
|
1075
1496
|
})
|
|
1076
1497
|
if (idx === -1) return null
|
|
1077
1498
|
const [taskId] = queue.splice(idx, 1)
|
|
@@ -1143,6 +1564,21 @@ export async function processNext() {
|
|
|
1143
1564
|
})
|
|
1144
1565
|
continue
|
|
1145
1566
|
}
|
|
1567
|
+
if (isAgentDisabled(agent)) {
|
|
1568
|
+
const now = Date.now()
|
|
1569
|
+
task.retryScheduledAt = now + DISABLED_AGENT_RETRY_MS
|
|
1570
|
+
task.updatedAt = now
|
|
1571
|
+
task.error = buildAgentDisabledMessage(agent, 'process queued tasks')
|
|
1572
|
+
saveTasks(tasks)
|
|
1573
|
+
notify('tasks')
|
|
1574
|
+
pushQueueUnique(queue, taskId)
|
|
1575
|
+
saveQueue(queue)
|
|
1576
|
+
pushMainLoopEventToMainSessions({
|
|
1577
|
+
type: 'task_deferred',
|
|
1578
|
+
text: `Task deferred: "${task.title}" (${task.id}) — agent ${task.agentId} is disabled.`,
|
|
1579
|
+
})
|
|
1580
|
+
continue
|
|
1581
|
+
}
|
|
1146
1582
|
|
|
1147
1583
|
// Mark as running
|
|
1148
1584
|
applyTaskPolicyDefaults(task)
|
|
@@ -1164,6 +1600,8 @@ export async function processNext() {
|
|
|
1164
1600
|
const sourceScheduleId = typeof scheduleTask.sourceScheduleId === 'string'
|
|
1165
1601
|
? scheduleTask.sourceScheduleId
|
|
1166
1602
|
: ''
|
|
1603
|
+
const reusableTaskSessionId = resolveReusableTaskSessionId(task, tasks as Record<string, BoardTask>, sessionsForCwd)
|
|
1604
|
+
const resumeContext = resolveTaskResumeContext(task, tasks as Record<string, BoardTask>, sessionsForCwd as Record<string, SessionLike | Session>)
|
|
1167
1605
|
|
|
1168
1606
|
// Resolve the agent's persistent thread session to use as parentSessionId
|
|
1169
1607
|
const agentThreadSessionId = agent.threadSessionId || null
|
|
@@ -1197,7 +1635,7 @@ export async function processNext() {
|
|
|
1197
1635
|
saveSchedules(schedules)
|
|
1198
1636
|
}
|
|
1199
1637
|
} else {
|
|
1200
|
-
sessionId = createOrchestratorSession(
|
|
1638
|
+
sessionId = reusableTaskSessionId || createOrchestratorSession(
|
|
1201
1639
|
agent,
|
|
1202
1640
|
task.title,
|
|
1203
1641
|
agentThreadSessionId || undefined,
|
|
@@ -1206,6 +1644,13 @@ export async function processNext() {
|
|
|
1206
1644
|
)
|
|
1207
1645
|
}
|
|
1208
1646
|
|
|
1647
|
+
const executionSessions = loadSessions() as Record<string, Session>
|
|
1648
|
+
const executionSession = executionSessions[sessionId]
|
|
1649
|
+
const seededResumeState = executionSession
|
|
1650
|
+
? applyTaskResumeStateToSession(executionSession, resumeContext?.resume)
|
|
1651
|
+
: false
|
|
1652
|
+
if (seededResumeState) saveSessions(executionSessions)
|
|
1653
|
+
|
|
1209
1654
|
// Notify the agent's thread that a task has started
|
|
1210
1655
|
if (agentThreadSessionId) {
|
|
1211
1656
|
try {
|
|
@@ -1231,9 +1676,19 @@ export async function processNext() {
|
|
|
1231
1676
|
}
|
|
1232
1677
|
|
|
1233
1678
|
task.sessionId = sessionId
|
|
1679
|
+
const reusedExistingSession = !isScheduleTask && Boolean(reusableTaskSessionId) && reusableTaskSessionId === sessionId
|
|
1680
|
+
const continuationBits: string[] = []
|
|
1681
|
+
if (reusedExistingSession) {
|
|
1682
|
+
continuationBits.push('reusing prior session')
|
|
1683
|
+
}
|
|
1684
|
+
if (resumeContext?.source === 'delegated_from_task' || resumeContext?.source === 'blocked_by') {
|
|
1685
|
+
continuationBits.push(`seeded from task ${resumeContext.sourceTaskId}`)
|
|
1686
|
+
} else if (seededResumeState) {
|
|
1687
|
+
continuationBits.push('restored CLI resume handles')
|
|
1688
|
+
}
|
|
1234
1689
|
task.checkpoint = {
|
|
1235
1690
|
lastSessionId: sessionId,
|
|
1236
|
-
note: `Attempt ${(task.attempts || 0) + 1}/${task.maxAttempts || '?'} started`,
|
|
1691
|
+
note: `Attempt ${(task.attempts || 0) + 1}/${task.maxAttempts || '?'} started${continuationBits.length ? ` (${continuationBits.join('; ')})` : ''}`,
|
|
1237
1692
|
updatedAt: Date.now(),
|
|
1238
1693
|
}
|
|
1239
1694
|
saveTasks(tasks)
|
|
@@ -1251,9 +1706,9 @@ export async function processNext() {
|
|
|
1251
1706
|
const delegatorId = (task as unknown as Record<string, unknown>).delegatedByAgentId as string | undefined
|
|
1252
1707
|
const delegator = delegatorId ? agents[delegatorId] : null
|
|
1253
1708
|
const prefix = `[delegation-source:${delegatorId || ''}:${delegator?.name || 'Agent'}:${delegator?.avatarSeed || ''}]`
|
|
1254
|
-
initialText = `${prefix}\nDelegated by **${delegator?.name || 'another agent'}** | [${task.title}](#task:${task.id})\n\n${task.description || ''}\n\nWorking directory: \`${taskCwd}
|
|
1709
|
+
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.`
|
|
1255
1710
|
} else {
|
|
1256
|
-
initialText = `Starting task: **${task.title}**\n\n${task.description || ''}\n\nWorking directory: \`${taskCwd}
|
|
1711
|
+
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.`
|
|
1257
1712
|
}
|
|
1258
1713
|
sessions[sessionId].messages.push({
|
|
1259
1714
|
role: 'assistant',
|
|
@@ -1379,6 +1834,7 @@ export async function processNext() {
|
|
|
1379
1834
|
})
|
|
1380
1835
|
notifyMainChatScheduleResult(doneTask)
|
|
1381
1836
|
notifyAgentThreadTaskResult(doneTask)
|
|
1837
|
+
cleanupTerminalOneOffSchedule(doneTask)
|
|
1382
1838
|
// Clean up LangGraph checkpoints for completed tasks
|
|
1383
1839
|
getCheckpointSaver().deleteThread(taskId).catch((e) =>
|
|
1384
1840
|
console.warn(`[queue] Failed to clean up checkpoints for task ${taskId}:`, e)
|
|
@@ -1406,6 +1862,7 @@ export async function processNext() {
|
|
|
1406
1862
|
if (doneTask?.status === 'failed') {
|
|
1407
1863
|
notifyMainChatScheduleResult(doneTask)
|
|
1408
1864
|
notifyAgentThreadTaskResult(doneTask)
|
|
1865
|
+
cleanupTerminalOneOffSchedule(doneTask)
|
|
1409
1866
|
}
|
|
1410
1867
|
console.warn(`[queue] Task "${task.title}" failed completion validation`)
|
|
1411
1868
|
}
|
|
@@ -1455,6 +1912,7 @@ export async function processNext() {
|
|
|
1455
1912
|
if (latest?.status === 'failed') {
|
|
1456
1913
|
notifyMainChatScheduleResult(latest)
|
|
1457
1914
|
notifyAgentThreadTaskResult(latest)
|
|
1915
|
+
cleanupTerminalOneOffSchedule(latest)
|
|
1458
1916
|
}
|
|
1459
1917
|
}
|
|
1460
1918
|
}
|
|
@@ -1492,14 +1950,15 @@ export function cleanupFinishedTaskSessions() {
|
|
|
1492
1950
|
|
|
1493
1951
|
/** Recover running tasks that appear stalled and requeue/dead-letter them per retry policy. */
|
|
1494
1952
|
export function recoverStalledRunningTasks(): { recovered: number; deadLettered: number } {
|
|
1953
|
+
const finished = reconcileFinishedRunningTasks()
|
|
1495
1954
|
const settings = loadSettings()
|
|
1496
1955
|
const stallTimeoutMin = normalizeInt(settings.taskStallTimeoutMin, 45, 5, 24 * 60)
|
|
1497
1956
|
const staleMs = stallTimeoutMin * 60_000
|
|
1498
1957
|
const now = Date.now()
|
|
1499
1958
|
const tasks = loadTasks()
|
|
1500
1959
|
const queue = loadQueue()
|
|
1501
|
-
let recovered =
|
|
1502
|
-
let deadLettered =
|
|
1960
|
+
let recovered = finished.reconciled
|
|
1961
|
+
let deadLettered = finished.deadLettered
|
|
1503
1962
|
let changed = false
|
|
1504
1963
|
|
|
1505
1964
|
for (const task of Object.values(tasks) as BoardTask[]) {
|