@swarmclawai/swarmclaw 0.7.8 → 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 -15
- 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 +22 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +26 -1
- 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/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/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 +73 -24
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +44 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- 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/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/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 +7 -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 +191 -95
- package/src/components/tasks/task-board.tsx +273 -2
- package/src/components/tasks/task-card.tsx +38 -9
- 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 +11 -0
- package/src/lib/server/capability-router.ts +26 -1
- 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 +353 -72
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +362 -63
- 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 +1 -1
- 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 +189 -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 +15 -10
- 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/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 +2 -2
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +205 -5
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +262 -0
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +293 -61
- 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 +52 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +348 -61
- package/src/lib/server/session-tools/context.ts +12 -3
- package/src/lib/server/session-tools/crud.ts +221 -10
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate.ts +64 -8
- 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/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +546 -79
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- 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 +162 -1
- package/src/lib/server/session-tools/web.ts +468 -64
- 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 +419 -9
- package/src/lib/server/stream-agent-chat.ts +887 -83
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- 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.ts +4 -2
- 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-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 +210 -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 { shouldAutoDeleteScheduleAfterTerminalRun } from '@/lib/schedule-origin'
|
|
16
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 : []
|
|
@@ -666,12 +683,10 @@ export function resolveTaskOriginConnectorFollowupTarget(params: {
|
|
|
666
683
|
const delegatedByAgentId = typeof metaTask.delegatedByAgentId === 'string'
|
|
667
684
|
? metaTask.delegatedByAgentId.trim()
|
|
668
685
|
: ''
|
|
686
|
+
const allowedOwners = new Set([task.agentId, delegatedByAgentId].filter(Boolean))
|
|
669
687
|
const sourceSessionId = typeof metaTask.createdInSessionId === 'string'
|
|
670
688
|
? metaTask.createdInSessionId.trim()
|
|
671
689
|
: ''
|
|
672
|
-
if (!sourceSessionId) return null
|
|
673
|
-
const sourceSession = sessions[sourceSessionId]
|
|
674
|
-
if (!sourceSession || !Array.isArray(sourceSession.messages)) return null
|
|
675
690
|
|
|
676
691
|
const runningById = new Map<string, RunningConnectorLike>()
|
|
677
692
|
for (const entry of running) {
|
|
@@ -679,9 +694,64 @@ export function resolveTaskOriginConnectorFollowupTarget(params: {
|
|
|
679
694
|
runningById.set(entry.id, entry)
|
|
680
695
|
}
|
|
681
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
|
+
|
|
682
751
|
for (let i = sourceSession.messages.length - 1; i >= 0; i--) {
|
|
683
752
|
const message = sourceSession.messages[i]
|
|
684
753
|
if (!message || message.role !== 'user') continue
|
|
754
|
+
if (message.historyExcluded === true) continue
|
|
685
755
|
|
|
686
756
|
const connectorId = typeof message.source?.connectorId === 'string'
|
|
687
757
|
? message.source.connectorId.trim()
|
|
@@ -690,15 +760,7 @@ export function resolveTaskOriginConnectorFollowupTarget(params: {
|
|
|
690
760
|
|
|
691
761
|
const connector = connectors[connectorId]
|
|
692
762
|
if (!connector) continue
|
|
693
|
-
const ownerId = typeof connector.agentId === 'string' ? connector.agentId.trim() : ''
|
|
694
|
-
if (ownerId) {
|
|
695
|
-
const allowedOwners = new Set([task.agentId, delegatedByAgentId].filter(Boolean))
|
|
696
|
-
if (!allowedOwners.has(ownerId)) continue
|
|
697
|
-
}
|
|
698
|
-
|
|
699
763
|
const runtime = runningById.get(connectorId)
|
|
700
|
-
if (runtime && !runtime.supportsSend) continue
|
|
701
|
-
|
|
702
764
|
const sourceChannel = typeof message.source?.channelId === 'string'
|
|
703
765
|
? message.source.channelId.trim()
|
|
704
766
|
: ''
|
|
@@ -707,20 +769,59 @@ export function resolveTaskOriginConnectorFollowupTarget(params: {
|
|
|
707
769
|
|| connector.config?.outboundJid
|
|
708
770
|
|| connector.config?.outboundTarget
|
|
709
771
|
|| ''
|
|
710
|
-
const
|
|
711
|
-
if (!rawChannel) continue
|
|
712
|
-
|
|
713
|
-
return {
|
|
772
|
+
const target = normalizeTarget({
|
|
714
773
|
connectorId,
|
|
715
|
-
channelId:
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
774
|
+
channelId: sourceChannel || fallbackChannel,
|
|
775
|
+
threadId: typeof message.source?.threadId === 'string' ? message.source.threadId : null,
|
|
776
|
+
})
|
|
777
|
+
if (target) return target
|
|
719
778
|
}
|
|
720
779
|
|
|
721
780
|
return null
|
|
722
781
|
}
|
|
723
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
|
+
|
|
724
825
|
// Task result extraction now uses Zod-validated structured data
|
|
725
826
|
// from ./task-result.ts (extractTaskResult, formatResultBody)
|
|
726
827
|
|
|
@@ -779,6 +880,132 @@ async function executeTaskRun(
|
|
|
779
880
|
return text
|
|
780
881
|
}
|
|
781
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
|
+
|
|
782
1009
|
function notifyMainChatScheduleResult(task: BoardTask): void {
|
|
783
1010
|
const scheduleTask = task as ScheduleTaskMeta
|
|
784
1011
|
const sourceType = typeof scheduleTask.sourceType === 'string' ? scheduleTask.sourceType : ''
|
|
@@ -844,6 +1071,22 @@ function notifyMainChatScheduleResult(task: BoardTask): void {
|
|
|
844
1071
|
if (changed) saveSessions(sessions)
|
|
845
1072
|
}
|
|
846
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
|
+
|
|
847
1090
|
async function notifyConnectorTaskFollowups(params: {
|
|
848
1091
|
task: BoardTask
|
|
849
1092
|
statusLabel: string
|
|
@@ -858,54 +1101,23 @@ async function notifyConnectorTaskFollowups(params: {
|
|
|
858
1101
|
const running = (await import('./connectors/manager')).listRunningConnectors()
|
|
859
1102
|
const manager = await import('./connectors/manager')
|
|
860
1103
|
const sessions = loadSessions()
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
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
|
|
869
1111
|
const originTarget = resolveTaskOriginConnectorFollowupTarget({
|
|
870
1112
|
task,
|
|
871
1113
|
sessions: sessions as Record<string, SessionLike>,
|
|
872
1114
|
connectors,
|
|
873
1115
|
running: running as RunningConnectorLike[],
|
|
874
1116
|
})
|
|
875
|
-
addCandidate(originTarget)
|
|
876
1117
|
const preferredTargetKey = originTarget
|
|
877
|
-
? `${originTarget.connectorId}|${originTarget.channelId}`
|
|
1118
|
+
? `${originTarget.connectorId}|${originTarget.channelId}|${originTarget.threadId || ''}`
|
|
878
1119
|
: ''
|
|
879
1120
|
|
|
880
|
-
for (const entry of running) {
|
|
881
|
-
if (!entry.supportsSend || !entry.id) continue
|
|
882
|
-
const connector = connectors[entry.id]
|
|
883
|
-
if (!connector) continue
|
|
884
|
-
if (connector.agentId !== task.agentId) continue
|
|
885
|
-
if (!isEnabledFlag(connector.config?.taskFollowups)) continue
|
|
886
|
-
const channelTargetRaw = entry.recentChannelId
|
|
887
|
-
|| entry.configuredTargets[0]
|
|
888
|
-
|| connector.config?.outboundJid
|
|
889
|
-
|| connector.config?.outboundTarget
|
|
890
|
-
|| ''
|
|
891
|
-
if (!channelTargetRaw) continue
|
|
892
|
-
addCandidate({
|
|
893
|
-
connectorId: entry.id,
|
|
894
|
-
channelId: connector.platform === 'whatsapp'
|
|
895
|
-
? normalizeWhatsappTarget(channelTargetRaw)
|
|
896
|
-
: channelTargetRaw,
|
|
897
|
-
})
|
|
898
|
-
}
|
|
899
|
-
const targets = [...candidateByKey.values()].sort((a, b) => {
|
|
900
|
-
if (!preferredTargetKey) return 0
|
|
901
|
-
const aKey = `${a.connectorId}|${a.channelId}`
|
|
902
|
-
const bKey = `${b.connectorId}|${b.channelId}`
|
|
903
|
-
if (aKey === preferredTargetKey && bKey !== preferredTargetKey) return -1
|
|
904
|
-
if (bKey === preferredTargetKey && aKey !== preferredTargetKey) return 1
|
|
905
|
-
return 0
|
|
906
|
-
})
|
|
907
|
-
if (!targets.length) return
|
|
908
|
-
|
|
909
1121
|
const summary = summaryText.trim().slice(0, 1400)
|
|
910
1122
|
for (const target of targets) {
|
|
911
1123
|
const connector = connectors[target.connectorId]
|
|
@@ -925,7 +1137,7 @@ async function notifyConnectorTaskFollowups(params: {
|
|
|
925
1137
|
`Task ${statusLabel}: ${task.title}`,
|
|
926
1138
|
summary || 'No summary provided.',
|
|
927
1139
|
].join('\n\n')
|
|
928
|
-
const targetKey = `${target.connectorId}|${target.channelId}`
|
|
1140
|
+
const targetKey = `${target.connectorId}|${target.channelId}|${target.threadId || ''}`
|
|
929
1141
|
const preferredChannelNote = !template && preferredTargetKey && targetKey === preferredTargetKey
|
|
930
1142
|
? '\n\n(Update sent in the same channel that requested this task.)'
|
|
931
1143
|
: ''
|
|
@@ -936,6 +1148,7 @@ async function notifyConnectorTaskFollowups(params: {
|
|
|
936
1148
|
await manager.sendConnectorMessage({
|
|
937
1149
|
connectorId: target.connectorId,
|
|
938
1150
|
channelId: target.channelId,
|
|
1151
|
+
threadId: target.threadId || undefined,
|
|
939
1152
|
text: outboundMessage,
|
|
940
1153
|
...(resolvedMediaPath
|
|
941
1154
|
? {
|
|
@@ -1351,6 +1564,21 @@ export async function processNext() {
|
|
|
1351
1564
|
})
|
|
1352
1565
|
continue
|
|
1353
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
|
+
}
|
|
1354
1582
|
|
|
1355
1583
|
// Mark as running
|
|
1356
1584
|
applyTaskPolicyDefaults(task)
|
|
@@ -1606,6 +1834,7 @@ export async function processNext() {
|
|
|
1606
1834
|
})
|
|
1607
1835
|
notifyMainChatScheduleResult(doneTask)
|
|
1608
1836
|
notifyAgentThreadTaskResult(doneTask)
|
|
1837
|
+
cleanupTerminalOneOffSchedule(doneTask)
|
|
1609
1838
|
// Clean up LangGraph checkpoints for completed tasks
|
|
1610
1839
|
getCheckpointSaver().deleteThread(taskId).catch((e) =>
|
|
1611
1840
|
console.warn(`[queue] Failed to clean up checkpoints for task ${taskId}:`, e)
|
|
@@ -1633,6 +1862,7 @@ export async function processNext() {
|
|
|
1633
1862
|
if (doneTask?.status === 'failed') {
|
|
1634
1863
|
notifyMainChatScheduleResult(doneTask)
|
|
1635
1864
|
notifyAgentThreadTaskResult(doneTask)
|
|
1865
|
+
cleanupTerminalOneOffSchedule(doneTask)
|
|
1636
1866
|
}
|
|
1637
1867
|
console.warn(`[queue] Task "${task.title}" failed completion validation`)
|
|
1638
1868
|
}
|
|
@@ -1682,6 +1912,7 @@ export async function processNext() {
|
|
|
1682
1912
|
if (latest?.status === 'failed') {
|
|
1683
1913
|
notifyMainChatScheduleResult(latest)
|
|
1684
1914
|
notifyAgentThreadTaskResult(latest)
|
|
1915
|
+
cleanupTerminalOneOffSchedule(latest)
|
|
1685
1916
|
}
|
|
1686
1917
|
}
|
|
1687
1918
|
}
|
|
@@ -1719,14 +1950,15 @@ export function cleanupFinishedTaskSessions() {
|
|
|
1719
1950
|
|
|
1720
1951
|
/** Recover running tasks that appear stalled and requeue/dead-letter them per retry policy. */
|
|
1721
1952
|
export function recoverStalledRunningTasks(): { recovered: number; deadLettered: number } {
|
|
1953
|
+
const finished = reconcileFinishedRunningTasks()
|
|
1722
1954
|
const settings = loadSettings()
|
|
1723
1955
|
const stallTimeoutMin = normalizeInt(settings.taskStallTimeoutMin, 45, 5, 24 * 60)
|
|
1724
1956
|
const staleMs = stallTimeoutMin * 60_000
|
|
1725
1957
|
const now = Date.now()
|
|
1726
1958
|
const tasks = loadTasks()
|
|
1727
1959
|
const queue = loadQueue()
|
|
1728
|
-
let recovered =
|
|
1729
|
-
let deadLettered =
|
|
1960
|
+
let recovered = finished.reconciled
|
|
1961
|
+
let deadLettered = finished.deadLettered
|
|
1730
1962
|
let changed = false
|
|
1731
1963
|
|
|
1732
1964
|
for (const task of Object.values(tasks) as BoardTask[]) {
|
|
@@ -7,6 +7,7 @@ import { getScheduleSignatureKey } from '@/lib/schedule-dedupe'
|
|
|
7
7
|
import { enqueueSystemEvent } from './system-events'
|
|
8
8
|
import { requestHeartbeatNow } from './heartbeat-wake'
|
|
9
9
|
import { processDueWatchJobs } from './watch-jobs'
|
|
10
|
+
import { isAgentDisabled } from './agent-availability'
|
|
10
11
|
|
|
11
12
|
const TICK_INTERVAL = 60_000 // 60 seconds
|
|
12
13
|
let intervalId: ReturnType<typeof setInterval> | null = null
|
|
@@ -32,6 +33,11 @@ interface SchedulerScheduleLike {
|
|
|
32
33
|
runNumber?: number
|
|
33
34
|
createdInSessionId?: string | null
|
|
34
35
|
createdByAgentId?: string | null
|
|
36
|
+
followupConnectorId?: string | null
|
|
37
|
+
followupChannelId?: string | null
|
|
38
|
+
followupThreadId?: string | null
|
|
39
|
+
followupSenderId?: string | null
|
|
40
|
+
followupSenderName?: string | null
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
export function startScheduler() {
|
|
@@ -123,6 +129,16 @@ async function tick() {
|
|
|
123
129
|
})
|
|
124
130
|
continue
|
|
125
131
|
}
|
|
132
|
+
if (isAgentDisabled(agent)) {
|
|
133
|
+
console.warn(`[scheduler] Skipping schedule "${schedule.name}" (${schedule.id}) because agent ${schedule.agentId} is disabled`)
|
|
134
|
+
advanceSchedule(schedule)
|
|
135
|
+
saveSchedules(schedules)
|
|
136
|
+
pushMainLoopEventToMainSessions({
|
|
137
|
+
type: 'schedule_skipped',
|
|
138
|
+
text: `Schedule skipped: "${schedule.name}" (${schedule.id}) — agent ${schedule.agentId} is disabled.`,
|
|
139
|
+
})
|
|
140
|
+
continue
|
|
141
|
+
}
|
|
126
142
|
|
|
127
143
|
console.log(`[scheduler] Firing schedule "${schedule.name}" (${schedule.id})`)
|
|
128
144
|
schedule.lastRunAt = now
|
|
@@ -185,6 +201,11 @@ async function tick() {
|
|
|
185
201
|
sourceScheduleKey: scheduleSignature || null,
|
|
186
202
|
createdInSessionId: schedule.createdInSessionId || null,
|
|
187
203
|
createdByAgentId: schedule.createdByAgentId || null,
|
|
204
|
+
followupConnectorId: schedule.followupConnectorId || null,
|
|
205
|
+
followupChannelId: schedule.followupChannelId || null,
|
|
206
|
+
followupThreadId: schedule.followupThreadId || null,
|
|
207
|
+
followupSenderId: schedule.followupSenderId || null,
|
|
208
|
+
followupSenderName: schedule.followupSenderName || null,
|
|
188
209
|
runNumber: schedule.runNumber,
|
|
189
210
|
}
|
|
190
211
|
schedule.linkedTaskId = taskId
|
|
@@ -204,6 +225,13 @@ async function tick() {
|
|
|
204
225
|
if (schedule.createdInSessionId) {
|
|
205
226
|
enqueueSystemEvent(schedule.createdInSessionId, `Schedule triggered: ${schedule.name}`)
|
|
206
227
|
}
|
|
207
|
-
requestHeartbeatNow({
|
|
228
|
+
requestHeartbeatNow({
|
|
229
|
+
agentId: schedule.agentId,
|
|
230
|
+
eventId: `${schedule.id}:${schedule.runNumber}`,
|
|
231
|
+
reason: 'schedule',
|
|
232
|
+
source: `schedule:${schedule.id}`,
|
|
233
|
+
resumeMessage: `Schedule triggered: ${schedule.name}`,
|
|
234
|
+
detail: `Run #${schedule.runNumber} queued task ${taskId}.`,
|
|
235
|
+
})
|
|
208
236
|
}
|
|
209
237
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { buildSessionNoteMessage } from './session-note'
|
|
5
|
+
|
|
6
|
+
test('buildSessionNoteMessage defaults to assistant/system note metadata', () => {
|
|
7
|
+
const result = buildSessionNoteMessage({
|
|
8
|
+
text: 'Live test passed',
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
assert.ok(result)
|
|
12
|
+
assert.equal(result?.role, 'assistant')
|
|
13
|
+
assert.equal(result?.kind, 'system')
|
|
14
|
+
assert.equal(result?.text, 'Live test passed')
|
|
15
|
+
assert.equal(typeof result?.time, 'number')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('buildSessionNoteMessage trims text and preserves explicit role/kind', () => {
|
|
19
|
+
const result = buildSessionNoteMessage({
|
|
20
|
+
text: ' Visible smoke report ',
|
|
21
|
+
role: 'user',
|
|
22
|
+
kind: 'chat',
|
|
23
|
+
time: 123,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
assert.deepEqual(result, {
|
|
27
|
+
role: 'user',
|
|
28
|
+
kind: 'chat',
|
|
29
|
+
text: 'Visible smoke report',
|
|
30
|
+
time: 123,
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('buildSessionNoteMessage returns null for empty text', () => {
|
|
35
|
+
assert.equal(buildSessionNoteMessage({ text: ' ' }), null)
|
|
36
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Message, MessageToolEvent } from '@/types'
|
|
2
|
+
import { loadSessions, saveSessions } from './storage'
|
|
3
|
+
import { notify } from './ws-hub'
|
|
4
|
+
|
|
5
|
+
export interface SessionNoteInput {
|
|
6
|
+
sessionId: string
|
|
7
|
+
text: string
|
|
8
|
+
role?: Message['role']
|
|
9
|
+
kind?: Message['kind']
|
|
10
|
+
toolEvents?: MessageToolEvent[]
|
|
11
|
+
time?: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function buildSessionNoteMessage(input: Omit<SessionNoteInput, 'sessionId'>): Message | null {
|
|
15
|
+
const trimmed = String(input.text || '').trim()
|
|
16
|
+
if (!trimmed) return null
|
|
17
|
+
return {
|
|
18
|
+
role: input.role || 'assistant',
|
|
19
|
+
kind: input.kind || 'system',
|
|
20
|
+
text: trimmed,
|
|
21
|
+
time: typeof input.time === 'number' && Number.isFinite(input.time) ? input.time : Date.now(),
|
|
22
|
+
...(Array.isArray(input.toolEvents) && input.toolEvents.length ? { toolEvents: input.toolEvents } : {}),
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function appendSessionNote(input: SessionNoteInput): Message | null {
|
|
27
|
+
const sessions = loadSessions()
|
|
28
|
+
const session = sessions[input.sessionId]
|
|
29
|
+
if (!session) return null
|
|
30
|
+
if (!Array.isArray(session.messages)) session.messages = []
|
|
31
|
+
|
|
32
|
+
const next = buildSessionNoteMessage(input)
|
|
33
|
+
if (!next) return null
|
|
34
|
+
|
|
35
|
+
session.messages.push(next)
|
|
36
|
+
session.lastActiveAt = next.time
|
|
37
|
+
sessions[input.sessionId] = session
|
|
38
|
+
saveSessions(sessions)
|
|
39
|
+
notify('sessions')
|
|
40
|
+
notify(`messages:${input.sessionId}`)
|
|
41
|
+
return next
|
|
42
|
+
}
|