@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
2
|
import { test } from 'node:test'
|
|
3
|
-
import { findDuplicateSchedule, getScheduleSignatureKey, type ScheduleLike } from './schedule-dedupe.ts'
|
|
3
|
+
import { findDuplicateSchedule, findEquivalentSchedules, getScheduleSignatureKey, type ScheduleLike } from './schedule-dedupe.ts'
|
|
4
4
|
|
|
5
5
|
test('findDuplicateSchedule matches active interval schedules with normalized prompts', () => {
|
|
6
6
|
const schedules: Record<string, ScheduleLike> = {
|
|
@@ -82,3 +82,68 @@ test('getScheduleSignatureKey is stable for equivalent schedules', () => {
|
|
|
82
82
|
assert.equal(keyA, keyB)
|
|
83
83
|
assert.notEqual(keyA, keyC)
|
|
84
84
|
})
|
|
85
|
+
|
|
86
|
+
test('findDuplicateSchedule fuzzy-matches same-session recurring reminders with different wording and cadence shape', () => {
|
|
87
|
+
const schedules: Record<string, ScheduleLike> = {
|
|
88
|
+
daily1: {
|
|
89
|
+
id: 'daily1',
|
|
90
|
+
agentId: 'assistant',
|
|
91
|
+
taskPrompt: 'Daily check for updates on US-Iran tensions',
|
|
92
|
+
scheduleType: 'cron',
|
|
93
|
+
cron: '0 9 * * *',
|
|
94
|
+
status: 'active',
|
|
95
|
+
createdByAgentId: 'assistant',
|
|
96
|
+
createdInSessionId: 'session-1',
|
|
97
|
+
createdAt: 1,
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const duplicate = findDuplicateSchedule(schedules, {
|
|
102
|
+
agentId: 'assistant',
|
|
103
|
+
taskPrompt: 'Periodic update check for US-Iran tensions',
|
|
104
|
+
scheduleType: 'interval',
|
|
105
|
+
intervalMs: 86_400_000,
|
|
106
|
+
createdByAgentId: 'assistant',
|
|
107
|
+
createdInSessionId: 'session-1',
|
|
108
|
+
}, {
|
|
109
|
+
creatorScope: {
|
|
110
|
+
agentId: 'assistant',
|
|
111
|
+
sessionId: 'session-1',
|
|
112
|
+
},
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
assert.ok(duplicate)
|
|
116
|
+
assert.equal(duplicate?.id, 'daily1')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('findEquivalentSchedules does not fuzzy-match across sessions', () => {
|
|
120
|
+
const schedules: Record<string, ScheduleLike> = {
|
|
121
|
+
daily1: {
|
|
122
|
+
id: 'daily1',
|
|
123
|
+
agentId: 'assistant',
|
|
124
|
+
taskPrompt: 'Daily check for updates on US-Iran tensions',
|
|
125
|
+
scheduleType: 'cron',
|
|
126
|
+
cron: '0 9 * * *',
|
|
127
|
+
status: 'active',
|
|
128
|
+
createdByAgentId: 'assistant',
|
|
129
|
+
createdInSessionId: 'session-1',
|
|
130
|
+
createdAt: 1,
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const matches = findEquivalentSchedules(schedules, {
|
|
135
|
+
agentId: 'assistant',
|
|
136
|
+
taskPrompt: 'Periodic update check for US-Iran tensions',
|
|
137
|
+
scheduleType: 'interval',
|
|
138
|
+
intervalMs: 86_400_000,
|
|
139
|
+
createdByAgentId: 'assistant',
|
|
140
|
+
createdInSessionId: 'session-2',
|
|
141
|
+
}, {
|
|
142
|
+
creatorScope: {
|
|
143
|
+
agentId: 'assistant',
|
|
144
|
+
sessionId: 'session-2',
|
|
145
|
+
},
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
assert.deepEqual(matches, [])
|
|
149
|
+
})
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CronExpressionParser } from 'cron-parser'
|
|
1
2
|
import type { ScheduleType } from '@/types'
|
|
2
3
|
|
|
3
4
|
export type ScheduleLike = {
|
|
@@ -41,12 +42,49 @@ interface ScheduleSignature {
|
|
|
41
42
|
id: string
|
|
42
43
|
agentId: string
|
|
43
44
|
taskPrompt: string
|
|
45
|
+
promptTokens: string[]
|
|
44
46
|
scheduleType: ScheduleType
|
|
45
47
|
cron: string
|
|
46
48
|
intervalMs: number | null
|
|
47
49
|
runAt: number | null
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
type ScheduleMatchKind = 'exact' | 'fuzzy'
|
|
53
|
+
|
|
54
|
+
const PROMPT_STOPWORDS = new Set([
|
|
55
|
+
'a',
|
|
56
|
+
'an',
|
|
57
|
+
'and',
|
|
58
|
+
'any',
|
|
59
|
+
'at',
|
|
60
|
+
'back',
|
|
61
|
+
'by',
|
|
62
|
+
'check',
|
|
63
|
+
'for',
|
|
64
|
+
'from',
|
|
65
|
+
'if',
|
|
66
|
+
'in',
|
|
67
|
+
'into',
|
|
68
|
+
'me',
|
|
69
|
+
'my',
|
|
70
|
+
'of',
|
|
71
|
+
'on',
|
|
72
|
+
'once',
|
|
73
|
+
'please',
|
|
74
|
+
'remind',
|
|
75
|
+
'report',
|
|
76
|
+
'task',
|
|
77
|
+
'the',
|
|
78
|
+
'this',
|
|
79
|
+
'to',
|
|
80
|
+
'up',
|
|
81
|
+
'update',
|
|
82
|
+
'updates',
|
|
83
|
+
'with',
|
|
84
|
+
])
|
|
85
|
+
|
|
86
|
+
const ONCE_MATCH_WINDOW_MS = 15 * 60 * 1000
|
|
87
|
+
|
|
50
88
|
function normalizeString(value: unknown): string {
|
|
51
89
|
return typeof value === 'string' ? value.trim() : ''
|
|
52
90
|
}
|
|
@@ -57,6 +95,26 @@ function normalizePrompt(value: unknown): string {
|
|
|
57
95
|
return text.replace(/\s+/g, ' ').trim().toLowerCase()
|
|
58
96
|
}
|
|
59
97
|
|
|
98
|
+
function normalizePromptToken(token: string): string {
|
|
99
|
+
let normalized = token
|
|
100
|
+
if (normalized.length > 4 && normalized.endsWith('ies')) normalized = `${normalized.slice(0, -3)}y`
|
|
101
|
+
else if (normalized.length > 5 && normalized.endsWith('ing')) normalized = normalized.slice(0, -3)
|
|
102
|
+
else if (normalized.length > 4 && normalized.endsWith('ed')) normalized = normalized.slice(0, -2)
|
|
103
|
+
else if (normalized.length > 3 && normalized.endsWith('s') && !normalized.endsWith('ss')) normalized = normalized.slice(0, -1)
|
|
104
|
+
return normalized
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function tokenizePrompt(value: unknown): string[] {
|
|
108
|
+
const normalized = normalizePrompt(value).replace(/[^a-z0-9]+/g, ' ')
|
|
109
|
+
if (!normalized) return []
|
|
110
|
+
return normalized
|
|
111
|
+
.split(' ')
|
|
112
|
+
.map((token) => normalizePromptToken(token.trim()))
|
|
113
|
+
.filter((token) => token.length > 0)
|
|
114
|
+
.filter((token) => token.length > 2 || ['ai', 'uk', 'us', 'eu'].includes(token))
|
|
115
|
+
.filter((token) => !PROMPT_STOPWORDS.has(token))
|
|
116
|
+
}
|
|
117
|
+
|
|
60
118
|
function normalizeCron(value: unknown): string {
|
|
61
119
|
const cron = normalizeString(value)
|
|
62
120
|
if (!cron) return ''
|
|
@@ -84,6 +142,7 @@ function toSignature(raw: ScheduleLike | ScheduleDuplicateCandidate): ScheduleSi
|
|
|
84
142
|
id: normalizeString(raw.id),
|
|
85
143
|
agentId: normalizeString(raw.agentId),
|
|
86
144
|
taskPrompt: normalizePrompt(raw.taskPrompt),
|
|
145
|
+
promptTokens: tokenizePrompt(raw.taskPrompt),
|
|
87
146
|
scheduleType: normalizeScheduleType(raw.scheduleType),
|
|
88
147
|
cron: normalizeCron(raw.cron),
|
|
89
148
|
intervalMs: normalizePositiveInt(raw.intervalMs),
|
|
@@ -116,6 +175,85 @@ function sameCadence(a: ScheduleSignature, b: ScheduleSignature): boolean {
|
|
|
116
175
|
return false
|
|
117
176
|
}
|
|
118
177
|
|
|
178
|
+
function tryResolveCronIntervalMs(cron: string): number | null {
|
|
179
|
+
if (!cron) return null
|
|
180
|
+
try {
|
|
181
|
+
const interval = CronExpressionParser.parse(cron, {
|
|
182
|
+
currentDate: new Date('2026-01-01T00:00:00.000Z'),
|
|
183
|
+
})
|
|
184
|
+
const first = interval.next().getTime()
|
|
185
|
+
const second = interval.next().getTime()
|
|
186
|
+
const diff = second - first
|
|
187
|
+
return diff > 0 ? diff : null
|
|
188
|
+
} catch {
|
|
189
|
+
return null
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function cadenceFamilyFromMs(intervalMs: number | null): string {
|
|
194
|
+
if (intervalMs == null || intervalMs <= 0) return ''
|
|
195
|
+
|
|
196
|
+
const families: Array<{ label: string; ms: number; toleranceMs: number }> = [
|
|
197
|
+
{ label: '15m', ms: 15 * 60 * 1000, toleranceMs: 60 * 1000 },
|
|
198
|
+
{ label: '30m', ms: 30 * 60 * 1000, toleranceMs: 2 * 60 * 1000 },
|
|
199
|
+
{ label: 'hourly', ms: 60 * 60 * 1000, toleranceMs: 5 * 60 * 1000 },
|
|
200
|
+
{ label: '6h', ms: 6 * 60 * 60 * 1000, toleranceMs: 15 * 60 * 1000 },
|
|
201
|
+
{ label: '12h', ms: 12 * 60 * 60 * 1000, toleranceMs: 30 * 60 * 1000 },
|
|
202
|
+
{ label: 'daily', ms: 24 * 60 * 60 * 1000, toleranceMs: 60 * 60 * 1000 },
|
|
203
|
+
{ label: 'weekly', ms: 7 * 24 * 60 * 60 * 1000, toleranceMs: 2 * 60 * 60 * 1000 },
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
for (const family of families) {
|
|
207
|
+
if (Math.abs(intervalMs - family.ms) <= family.toleranceMs) return family.label
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return `interval:${Math.round(intervalMs / 60_000)}m`
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function cadenceFamily(signature: ScheduleSignature): string {
|
|
214
|
+
if (signature.scheduleType === 'once') return signature.runAt != null ? 'once' : ''
|
|
215
|
+
if (signature.scheduleType === 'interval') return cadenceFamilyFromMs(signature.intervalMs)
|
|
216
|
+
if (signature.scheduleType === 'cron') return cadenceFamilyFromMs(tryResolveCronIntervalMs(signature.cron))
|
|
217
|
+
return ''
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function sameCadenceFamily(a: ScheduleSignature, b: ScheduleSignature): boolean {
|
|
221
|
+
if (sameCadence(a, b)) return true
|
|
222
|
+
if (a.scheduleType === 'once' && b.scheduleType === 'once') {
|
|
223
|
+
if (a.runAt == null || b.runAt == null) return false
|
|
224
|
+
return Math.abs(a.runAt - b.runAt) <= ONCE_MATCH_WINDOW_MS
|
|
225
|
+
}
|
|
226
|
+
if (a.scheduleType === 'once' || b.scheduleType === 'once') return false
|
|
227
|
+
const aFamily = cadenceFamily(a)
|
|
228
|
+
const bFamily = cadenceFamily(b)
|
|
229
|
+
return aFamily !== '' && aFamily === bFamily
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function countTokenOverlap(a: string[], b: string[]): number {
|
|
233
|
+
if (!a.length || !b.length) return 0
|
|
234
|
+
const smaller = a.length <= b.length ? a : b
|
|
235
|
+
const largerSet = new Set(a.length <= b.length ? b : a)
|
|
236
|
+
let overlap = 0
|
|
237
|
+
for (const token of new Set(smaller)) {
|
|
238
|
+
if (largerSet.has(token)) overlap += 1
|
|
239
|
+
}
|
|
240
|
+
return overlap
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function hasFuzzyPromptMatch(a: ScheduleSignature, b: ScheduleSignature): boolean {
|
|
244
|
+
if (!a.promptTokens.length || !b.promptTokens.length) return false
|
|
245
|
+
const uniqueA = [...new Set(a.promptTokens)]
|
|
246
|
+
const uniqueB = [...new Set(b.promptTokens)]
|
|
247
|
+
const overlap = countTokenOverlap(uniqueA, uniqueB)
|
|
248
|
+
if (overlap === 0) return false
|
|
249
|
+
const smallerSize = Math.min(uniqueA.length, uniqueB.length)
|
|
250
|
+
const largerSize = Math.max(uniqueA.length, uniqueB.length)
|
|
251
|
+
const coverage = overlap / smallerSize
|
|
252
|
+
const jaccard = overlap / new Set([...uniqueA, ...uniqueB]).size
|
|
253
|
+
if (smallerSize <= 2) return overlap === smallerSize
|
|
254
|
+
return overlap >= 2 && coverage >= 0.67 && (jaccard >= 0.5 || overlap >= Math.max(2, largerSize - 1))
|
|
255
|
+
}
|
|
256
|
+
|
|
119
257
|
function isEligibleStatus(status: unknown, includeStatuses: Set<string>): boolean {
|
|
120
258
|
const normalized = normalizeString(status).toLowerCase() || 'active'
|
|
121
259
|
return includeStatuses.has(normalized)
|
|
@@ -149,26 +287,45 @@ export function findDuplicateSchedule(
|
|
|
149
287
|
candidateRaw: ScheduleDuplicateCandidate,
|
|
150
288
|
opts: FindDuplicateScheduleOptions = {},
|
|
151
289
|
): ScheduleLike | null {
|
|
290
|
+
return findEquivalentSchedules(schedules, candidateRaw, opts)[0] || null
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function findEquivalentSchedules(
|
|
294
|
+
schedules: Record<string, ScheduleLike>,
|
|
295
|
+
candidateRaw: ScheduleDuplicateCandidate,
|
|
296
|
+
opts: FindDuplicateScheduleOptions = {},
|
|
297
|
+
): ScheduleLike[] {
|
|
152
298
|
const candidate = toSignature(candidateRaw)
|
|
153
|
-
if (!candidate.agentId) return
|
|
154
|
-
if (!candidate.taskPrompt) return
|
|
299
|
+
if (!candidate.agentId) return []
|
|
300
|
+
if (!candidate.taskPrompt) return []
|
|
155
301
|
|
|
156
302
|
const ignoreId = normalizeString(opts.ignoreId || candidate.id)
|
|
157
303
|
const statuses = new Set((opts.includeStatuses?.length ? opts.includeStatuses : ['active', 'paused']).map((s) => s.toLowerCase()))
|
|
304
|
+
const scopeSessionId = normalizeString(opts.creatorScope?.sessionId)
|
|
158
305
|
|
|
159
306
|
const matches = Object.values(schedules)
|
|
160
307
|
.filter((existing) => existing && typeof existing === 'object')
|
|
161
|
-
.
|
|
308
|
+
.map((existing) => {
|
|
162
309
|
const signature = toSignature(existing)
|
|
163
|
-
if (!signature.id) return
|
|
164
|
-
if (ignoreId && signature.id === ignoreId) return
|
|
165
|
-
if (!isEligibleStatus(existing.status, statuses)) return
|
|
166
|
-
if (!matchesCreatorScope(existing, opts.creatorScope || null)) return
|
|
167
|
-
if (signature.agentId !== candidate.agentId) return
|
|
168
|
-
|
|
169
|
-
return
|
|
310
|
+
if (!signature.id) return null
|
|
311
|
+
if (ignoreId && signature.id === ignoreId) return null
|
|
312
|
+
if (!isEligibleStatus(existing.status, statuses)) return null
|
|
313
|
+
if (!matchesCreatorScope(existing, opts.creatorScope || null)) return null
|
|
314
|
+
if (signature.agentId !== candidate.agentId) return null
|
|
315
|
+
const exact = signature.taskPrompt === candidate.taskPrompt && sameCadence(signature, candidate)
|
|
316
|
+
if (exact) return { existing, kind: 'exact' as const }
|
|
317
|
+
const fuzzy = Boolean(scopeSessionId)
|
|
318
|
+
&& hasFuzzyPromptMatch(signature, candidate)
|
|
319
|
+
&& sameCadenceFamily(signature, candidate)
|
|
320
|
+
if (!fuzzy) return null
|
|
321
|
+
return { existing, kind: 'fuzzy' as const }
|
|
322
|
+
})
|
|
323
|
+
.filter((entry): entry is { existing: ScheduleLike; kind: ScheduleMatchKind } => Boolean(entry))
|
|
324
|
+
.sort((a, b) => {
|
|
325
|
+
if (a.kind !== b.kind) return a.kind === 'exact' ? -1 : 1
|
|
326
|
+
return compareUpdatedDesc(a.existing, b.existing)
|
|
170
327
|
})
|
|
171
|
-
.
|
|
328
|
+
.map((entry) => entry.existing)
|
|
172
329
|
|
|
173
|
-
return matches
|
|
330
|
+
return matches
|
|
174
331
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { test } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
isAgentCreatedSchedule,
|
|
6
|
+
isUserCreatedSchedule,
|
|
7
|
+
shouldAutoDeleteScheduleAfterTerminalRun,
|
|
8
|
+
} from './schedule-origin'
|
|
9
|
+
|
|
10
|
+
test('recognizes agent-created schedules', () => {
|
|
11
|
+
assert.equal(isAgentCreatedSchedule({ scheduleType: 'interval', createdByAgentId: 'molly-2' }), true)
|
|
12
|
+
assert.equal(isUserCreatedSchedule({ scheduleType: 'interval', createdByAgentId: 'molly-2' }), false)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('recognizes manual schedules and only auto-deletes agent-created one-offs', () => {
|
|
16
|
+
assert.equal(isUserCreatedSchedule({ scheduleType: 'once', createdByAgentId: null }), true)
|
|
17
|
+
assert.equal(shouldAutoDeleteScheduleAfterTerminalRun({ scheduleType: 'once', createdByAgentId: null }), false)
|
|
18
|
+
assert.equal(shouldAutoDeleteScheduleAfterTerminalRun({ scheduleType: 'once', createdByAgentId: 'molly-2' }), true)
|
|
19
|
+
assert.equal(shouldAutoDeleteScheduleAfterTerminalRun({ scheduleType: 'interval', createdByAgentId: 'molly-2' }), false)
|
|
20
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Schedule } from '@/types'
|
|
2
|
+
|
|
3
|
+
type ScheduleOriginShape = Pick<Schedule, 'scheduleType' | 'createdByAgentId'>
|
|
4
|
+
|
|
5
|
+
export function isAgentCreatedSchedule(schedule: ScheduleOriginShape | null | undefined): boolean {
|
|
6
|
+
return Boolean(typeof schedule?.createdByAgentId === 'string' && schedule.createdByAgentId.trim())
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function isUserCreatedSchedule(schedule: ScheduleOriginShape | null | undefined): boolean {
|
|
10
|
+
return !isAgentCreatedSchedule(schedule)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function shouldAutoDeleteScheduleAfterTerminalRun(schedule: ScheduleOriginShape | null | undefined): boolean {
|
|
14
|
+
return Boolean(schedule?.scheduleType === 'once' && isAgentCreatedSchedule(schedule))
|
|
15
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
|
|
5
|
+
const server = new McpServer({
|
|
6
|
+
name: 'swarmclaw-fake-mcp',
|
|
7
|
+
version: '1.0.0',
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
server.registerTool('ping', {
|
|
11
|
+
description: 'Returns pong for smoke tests',
|
|
12
|
+
inputSchema: {},
|
|
13
|
+
}, async () => ({
|
|
14
|
+
content: [{ type: 'text', text: 'pong' }],
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
server.registerTool('echo', {
|
|
18
|
+
description: 'Echoes a caller-provided message',
|
|
19
|
+
inputSchema: {
|
|
20
|
+
message: z.string(),
|
|
21
|
+
},
|
|
22
|
+
}, async ({ message }) => ({
|
|
23
|
+
content: [{ type: 'text', text: `echo: ${message}` }],
|
|
24
|
+
}))
|
|
25
|
+
|
|
26
|
+
const transport = new StdioServerTransport()
|
|
27
|
+
await server.connect(transport)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Agent } from '@/types'
|
|
2
|
+
|
|
3
|
+
export function isAgentDisabled(agent: Pick<Agent, 'disabled'> | null | undefined): boolean {
|
|
4
|
+
return agent?.disabled === true
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function buildAgentDisabledMessage(
|
|
8
|
+
agent: Pick<Agent, 'name'> | null | undefined,
|
|
9
|
+
action?: string,
|
|
10
|
+
): string {
|
|
11
|
+
const name = typeof agent?.name === 'string' && agent.name.trim()
|
|
12
|
+
? agent.name.trim()
|
|
13
|
+
: 'This agent'
|
|
14
|
+
if (action) return `${name} is disabled and cannot ${action}. Re-enable it to continue.`
|
|
15
|
+
return `${name} is disabled. Re-enable it to continue.`
|
|
16
|
+
}
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
ProviderType,
|
|
7
7
|
} from '@/types'
|
|
8
8
|
import { deriveOpenClawWsUrl, normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
|
|
9
|
+
import { getProvider } from '@/lib/providers'
|
|
9
10
|
import { loadGatewayProfiles } from './storage'
|
|
10
11
|
import { isProviderCoolingDown } from './provider-health'
|
|
11
12
|
|
|
@@ -75,6 +76,8 @@ function normalizeGatewayDeployment(
|
|
|
75
76
|
useCase: normalizeText(deployment.useCase) as DeploymentConfig['useCase'],
|
|
76
77
|
exposure: normalizeText(deployment.exposure) as DeploymentConfig['exposure'],
|
|
77
78
|
managedBy: normalizeText(deployment.managedBy) as DeploymentConfig['managedBy'],
|
|
79
|
+
localInstanceId: normalizeText(deployment.localInstanceId),
|
|
80
|
+
localPort: normalizeNullableNumber(deployment.localPort),
|
|
78
81
|
targetHost: normalizeText(deployment.targetHost),
|
|
79
82
|
sshHost: normalizeText(deployment.sshHost),
|
|
80
83
|
sshUser: normalizeText(deployment.sshUser),
|
|
@@ -247,6 +250,12 @@ function dedupeCredentialIds(primary: string | null | undefined, candidates: str
|
|
|
247
250
|
return result
|
|
248
251
|
}
|
|
249
252
|
|
|
253
|
+
function resolveProviderDefaultEndpoint(provider: string): string | null {
|
|
254
|
+
const info = getProvider(provider)
|
|
255
|
+
if (!info?.defaultEndpoint) return null
|
|
256
|
+
return normalizeProviderEndpoint(provider, info.defaultEndpoint) || info.defaultEndpoint.replace(/\/+$/, '')
|
|
257
|
+
}
|
|
258
|
+
|
|
250
259
|
function buildRouteFromSeed(
|
|
251
260
|
seed: RouteSeed,
|
|
252
261
|
gatewayProfiles: GatewayProfile[],
|
|
@@ -267,10 +276,9 @@ function buildRouteFromSeed(
|
|
|
267
276
|
const gatewayProfileId = gatewayProfile?.id ?? seed.gatewayProfileId ?? agentGatewayProfileId ?? null
|
|
268
277
|
|
|
269
278
|
const providerFromGateway = gatewayProfile?.provider === 'openclaw' ? 'openclaw' : provider
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
)
|
|
279
|
+
const explicitEndpoint = seed.apiEndpoint ?? gatewayProfile?.endpoint ?? null
|
|
280
|
+
const apiEndpoint = normalizeProviderEndpoint(providerFromGateway, explicitEndpoint)
|
|
281
|
+
?? resolveProviderDefaultEndpoint(providerFromGateway)
|
|
274
282
|
const model = (seed.model || '').trim() || (providerFromGateway === 'openclaw' ? DEFAULT_OPENCLAW_MODEL : '')
|
|
275
283
|
if (!providerFromGateway || !model) return null
|
|
276
284
|
|
|
@@ -56,6 +56,9 @@ describe('ensureAgentThreadSession', () => {
|
|
|
56
56
|
fallbackCredentialIds: [],
|
|
57
57
|
heartbeatEnabled: true,
|
|
58
58
|
heartbeatIntervalSec: 600,
|
|
59
|
+
memoryScopeMode: 'agent',
|
|
60
|
+
memoryTierPreference: 'blended',
|
|
61
|
+
projectId: 'proj-1',
|
|
59
62
|
createdAt: now,
|
|
60
63
|
updatedAt: now,
|
|
61
64
|
plugins: ['memory', 'web_search'],
|
|
@@ -80,6 +83,54 @@ describe('ensureAgentThreadSession', () => {
|
|
|
80
83
|
assert.equal(output.session.shortcutForAgentId, 'molly')
|
|
81
84
|
assert.equal(output.session.agentId, 'molly')
|
|
82
85
|
assert.equal(output.session.heartbeatEnabled, true)
|
|
86
|
+
assert.equal(output.session.memoryScopeMode, 'agent')
|
|
87
|
+
assert.equal(output.session.memoryTierPreference, 'blended')
|
|
88
|
+
assert.equal(output.session.projectId, 'proj-1')
|
|
83
89
|
assert.deepEqual(output.session.plugins, ['memory', 'web_search'])
|
|
84
90
|
})
|
|
91
|
+
|
|
92
|
+
it('does not create a new shortcut chat when the agent is disabled', () => {
|
|
93
|
+
const output = runWithTempDataDir(`
|
|
94
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
95
|
+
const storage = storageMod.default || storageMod['module.exports'] || storageMod
|
|
96
|
+
const helperMod = await import('./src/lib/server/agent-thread-session.ts')
|
|
97
|
+
const ensureAgentThreadSession = helperMod.ensureAgentThreadSession
|
|
98
|
+
|| helperMod.default?.ensureAgentThreadSession
|
|
99
|
+
|| helperMod['module.exports']?.ensureAgentThreadSession
|
|
100
|
+
|
|
101
|
+
const now = Date.now()
|
|
102
|
+
storage.saveAgents({
|
|
103
|
+
molly: {
|
|
104
|
+
id: 'molly',
|
|
105
|
+
name: 'Molly',
|
|
106
|
+
description: 'Temporarily disabled helper',
|
|
107
|
+
provider: 'openai',
|
|
108
|
+
model: 'gpt-test',
|
|
109
|
+
credentialId: null,
|
|
110
|
+
apiEndpoint: null,
|
|
111
|
+
fallbackCredentialIds: [],
|
|
112
|
+
disabled: true,
|
|
113
|
+
heartbeatEnabled: true,
|
|
114
|
+
heartbeatIntervalSec: 600,
|
|
115
|
+
createdAt: now,
|
|
116
|
+
updatedAt: now,
|
|
117
|
+
plugins: ['memory'],
|
|
118
|
+
},
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
const session = ensureAgentThreadSession('molly')
|
|
122
|
+
const agents = storage.loadAgents()
|
|
123
|
+
const sessions = storage.loadSessions()
|
|
124
|
+
|
|
125
|
+
console.log(JSON.stringify({
|
|
126
|
+
sessionId: session?.id || null,
|
|
127
|
+
threadSessionId: agents.molly?.threadSessionId || null,
|
|
128
|
+
sessionCount: Object.keys(sessions).length,
|
|
129
|
+
}))
|
|
130
|
+
`)
|
|
131
|
+
|
|
132
|
+
assert.equal(output.sessionId, null)
|
|
133
|
+
assert.equal(output.threadSessionId, null)
|
|
134
|
+
assert.equal(output.sessionCount, 0)
|
|
135
|
+
})
|
|
85
136
|
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { genId } from '@/lib/id'
|
|
2
2
|
import type { Agent, Session } from '@/types'
|
|
3
3
|
import { applyResolvedRoute, resolvePrimaryAgentRoute } from './agent-runtime-config'
|
|
4
|
+
import { isAgentDisabled } from './agent-availability'
|
|
4
5
|
import { WORKSPACE_DIR } from './data-dir'
|
|
5
6
|
import { loadAgents, loadSessions, saveAgents, saveSessions } from './storage'
|
|
6
7
|
|
|
@@ -44,6 +45,9 @@ function buildThreadSession(agent: Agent, sessionId: string, user: string, creat
|
|
|
44
45
|
heartbeatEnabled: agent.heartbeatEnabled || false,
|
|
45
46
|
heartbeatIntervalSec: agent.heartbeatIntervalSec || null,
|
|
46
47
|
heartbeatTarget: existing?.heartbeatTarget || null,
|
|
48
|
+
memoryScopeMode: agent.memoryScopeMode || null,
|
|
49
|
+
memoryTierPreference: agent.memoryTierPreference || null,
|
|
50
|
+
projectId: agent.projectId || existing?.projectId || null,
|
|
47
51
|
sessionResetMode: existing?.sessionResetMode || null,
|
|
48
52
|
sessionIdleTimeoutSec: existing?.sessionIdleTimeoutSec || null,
|
|
49
53
|
sessionMaxAgeSec: existing?.sessionMaxAgeSec || null,
|
|
@@ -95,6 +99,7 @@ export function ensureAgentThreadSession(agentId: string, user = 'default'): Ses
|
|
|
95
99
|
|
|
96
100
|
const sessions = loadSessions()
|
|
97
101
|
const now = Date.now()
|
|
102
|
+
const disabled = isAgentDisabled(agent)
|
|
98
103
|
|
|
99
104
|
const existingId = typeof agent.threadSessionId === 'string' ? agent.threadSessionId : ''
|
|
100
105
|
if (existingId && sessions[existingId]) {
|
|
@@ -119,6 +124,8 @@ export function ensureAgentThreadSession(agentId: string, user = 'default'): Ses
|
|
|
119
124
|
return session
|
|
120
125
|
}
|
|
121
126
|
|
|
127
|
+
if (disabled) return null
|
|
128
|
+
|
|
122
129
|
const sessionId = `agent-chat-${agentId}-${genId()}`
|
|
123
130
|
const session = buildThreadSession(agent, sessionId, user, now)
|
|
124
131
|
sessions[sessionId] = session
|