@swarmclawai/swarmclaw 1.2.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/package.json +4 -1
- package/src/app/api/chats/[id]/deploy/route.ts +11 -6
- package/src/app/api/chats/[id]/devserver/route.ts +5 -2
- package/src/app/api/chats/[id]/messages/route.ts +7 -1
- package/src/app/api/credentials/[id]/route.ts +4 -1
- package/src/app/api/extensions/marketplace/route.ts +5 -2
- package/src/app/api/memory/maintenance/route.ts +5 -2
- package/src/app/api/preview-server/route.ts +14 -11
- package/src/app/api/system/status/route.ts +11 -0
- package/src/app/api/upload/route.ts +4 -1
- package/src/cli/index.js +7 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-files-editor.tsx +44 -32
- package/src/components/agents/personality-builder.tsx +13 -7
- package/src/components/agents/trash-list.tsx +1 -1
- package/src/components/chat/message-bubble.tsx +1 -0
- package/src/components/chat/message-list.tsx +25 -39
- package/src/components/chat/swarm-status-card.tsx +10 -3
- package/src/components/layout/daemon-indicator.tsx +7 -8
- package/src/components/layout/update-banner.tsx +8 -13
- package/src/components/logs/log-list.tsx +1 -1
- package/src/components/memory/memory-card.tsx +3 -1
- package/src/components/org-chart/org-chart-view.tsx +4 -0
- package/src/components/projects/project-list.tsx +4 -2
- package/src/components/projects/tabs/overview-tab.tsx +3 -2
- package/src/components/secrets/secret-sheet.tsx +1 -1
- package/src/components/secrets/secrets-list.tsx +1 -1
- package/src/components/shared/agent-switch-dialog.tsx +12 -6
- package/src/components/shared/dir-browser.tsx +22 -18
- package/src/components/skills/skill-sheet.tsx +2 -3
- package/src/components/tasks/task-list.tsx +1 -1
- package/src/components/tasks/task-sheet.tsx +1 -1
- package/src/hooks/use-openclaw-gateway.ts +46 -27
- package/src/instrumentation.ts +10 -7
- package/src/lib/chat/chat.ts +18 -2
- package/src/lib/providers/anthropic.ts +6 -3
- package/src/lib/providers/claude-cli.ts +9 -3
- package/src/lib/providers/cli-utils.ts +15 -0
- package/src/lib/providers/codex-cli.ts +9 -3
- package/src/lib/providers/gemini-cli.ts +6 -2
- package/src/lib/providers/index.ts +4 -1
- package/src/lib/providers/ollama.ts +5 -2
- package/src/lib/providers/openai.ts +8 -5
- package/src/lib/providers/opencode-cli.ts +6 -2
- package/src/lib/server/agents/agent-registry.ts +20 -3
- package/src/lib/server/agents/main-agent-loop.ts +4 -3
- package/src/lib/server/autonomy/supervisor-reflection.ts +14 -1
- package/src/lib/server/chat-execution/chat-execution.ts +14 -2
- package/src/lib/server/chat-execution/continuation-evaluator.ts +4 -3
- package/src/lib/server/chat-execution/continuation-limits.ts +6 -3
- package/src/lib/server/chat-execution/message-classifier.ts +5 -2
- package/src/lib/server/chat-execution/post-stream-finalization.ts +4 -1
- package/src/lib/server/chat-execution/prompt-builder.ts +11 -1
- package/src/lib/server/chat-execution/prompt-sections.ts +52 -9
- package/src/lib/server/chat-execution/response-completeness.ts +5 -2
- package/src/lib/server/chat-execution/stream-agent-chat.ts +42 -12
- package/src/lib/server/chatrooms/chatroom-memory-bridge.ts +6 -3
- package/src/lib/server/connectors/bluebubbles.ts +7 -4
- package/src/lib/server/connectors/connector-inbound.ts +16 -13
- package/src/lib/server/connectors/connector-lifecycle.ts +11 -8
- package/src/lib/server/connectors/connector-outbound.ts +6 -3
- package/src/lib/server/connectors/discord.ts +10 -7
- package/src/lib/server/connectors/email.ts +17 -14
- package/src/lib/server/connectors/googlechat.ts +7 -4
- package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -2
- package/src/lib/server/connectors/matrix.ts +6 -3
- package/src/lib/server/connectors/openclaw.ts +20 -17
- package/src/lib/server/connectors/outbox.ts +4 -1
- package/src/lib/server/connectors/runtime-state.ts +19 -0
- package/src/lib/server/connectors/session-consolidation.ts +5 -2
- package/src/lib/server/connectors/signal.ts +9 -6
- package/src/lib/server/connectors/slack.ts +13 -10
- package/src/lib/server/connectors/teams.ts +8 -5
- package/src/lib/server/connectors/telegram.ts +15 -12
- package/src/lib/server/connectors/whatsapp.ts +32 -29
- package/src/lib/server/embeddings.ts +4 -1
- package/src/lib/server/link-understanding.ts +4 -1
- package/src/lib/server/memory/memory-abstract.ts +59 -0
- package/src/lib/server/memory/memory-db.ts +40 -14
- package/src/lib/server/missions/mission-service.ts +6 -3
- package/src/lib/server/openclaw/gateway.ts +8 -5
- package/src/lib/server/project-utils.ts +13 -0
- package/src/lib/server/protocols/protocol-agent-turn.ts +5 -2
- package/src/lib/server/protocols/protocol-run-lifecycle.ts +5 -2
- package/src/lib/server/protocols/protocol-step-helpers.ts +4 -1
- package/src/lib/server/provider-health.ts +18 -0
- package/src/lib/server/query-expansion.ts +4 -1
- package/src/lib/server/runtime/alert-dispatch.ts +7 -6
- package/src/lib/server/runtime/daemon-state.ts +189 -50
- package/src/lib/server/runtime/heartbeat-service.ts +23 -0
- package/src/lib/server/runtime/idle-window.ts +4 -1
- package/src/lib/server/runtime/perf.ts +4 -1
- package/src/lib/server/runtime/process-manager.ts +7 -4
- package/src/lib/server/runtime/queue.ts +31 -28
- package/src/lib/server/runtime/scheduler.ts +9 -6
- package/src/lib/server/runtime/session-run-manager.ts +3 -0
- package/src/lib/server/sandbox/bridge-auth-registry.ts +6 -0
- package/src/lib/server/sandbox/novnc-auth.ts +10 -0
- package/src/lib/server/session-tools/context.ts +14 -0
- package/src/lib/server/session-tools/discovery.ts +9 -6
- package/src/lib/server/session-tools/index.ts +3 -1
- package/src/lib/server/session-tools/platform.ts +1 -1
- package/src/lib/server/session-tools/subagent.ts +23 -2
- package/src/lib/server/session-tools/wallet.ts +4 -1
- package/src/lib/server/skills/clawhub-client.ts +4 -1
- package/src/lib/server/skills/runtime-skill-resolver.ts +8 -2
- package/src/lib/server/skills/skill-eligibility.ts +6 -0
- package/src/lib/server/solana.ts +6 -0
- package/src/lib/server/storage-auth.ts +5 -5
- package/src/lib/server/storage-normalization.ts +4 -0
- package/src/lib/server/storage.ts +19 -8
- package/src/lib/server/tasks/task-followups.ts +4 -1
- package/src/lib/server/tool-loop-detection.ts +8 -3
- package/src/lib/server/tool-planning.ts +226 -0
- package/src/lib/server/tool-retry.ts +4 -3
- package/src/lib/server/wallet/wallet-portfolio.ts +29 -0
- package/src/lib/server/ws-hub.ts +5 -2
- package/src/lib/strip-internal-metadata.test.ts +44 -4
- package/src/lib/strip-internal-metadata.ts +20 -6
- package/src/stores/use-approval-store.ts +7 -1
- package/src/stores/use-chat-store.ts +5 -1
- package/src/types/index.ts +6 -0
|
@@ -1157,3 +1157,26 @@ export function pruneHeartbeatState(liveSessionIds: Set<string>): number {
|
|
|
1157
1157
|
}
|
|
1158
1158
|
return removed
|
|
1159
1159
|
}
|
|
1160
|
+
|
|
1161
|
+
/**
|
|
1162
|
+
* Remove orchestrator tracking entries for agents that no longer exist.
|
|
1163
|
+
* Called periodically by the daemon health sweep.
|
|
1164
|
+
*/
|
|
1165
|
+
export function pruneOrchestratorState(liveAgentIds: Set<string>): number {
|
|
1166
|
+
let removed = 0
|
|
1167
|
+
for (const agentId of orchestratorState.lastWakeByAgent.keys()) {
|
|
1168
|
+
if (!liveAgentIds.has(agentId)) { orchestratorState.lastWakeByAgent.delete(agentId); removed++ }
|
|
1169
|
+
}
|
|
1170
|
+
for (const agentId of orchestratorState.failures.keys()) {
|
|
1171
|
+
if (!liveAgentIds.has(agentId)) { orchestratorState.failures.delete(agentId); removed++ }
|
|
1172
|
+
}
|
|
1173
|
+
const todayStr = new Date().toISOString().slice(0, 10)
|
|
1174
|
+
for (const key of orchestratorState.dailyCycles.keys()) {
|
|
1175
|
+
const agentId = key.slice(0, key.lastIndexOf(':'))
|
|
1176
|
+
if (!liveAgentIds.has(agentId) || key.slice(key.lastIndexOf(':') + 1) !== todayStr) {
|
|
1177
|
+
orchestratorState.dailyCycles.delete(key)
|
|
1178
|
+
removed++
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
return removed
|
|
1182
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { loadSessions } from '@/lib/server/storage'
|
|
2
2
|
import type { Session } from '@/types'
|
|
3
|
+
import { log } from '@/lib/server/logger'
|
|
4
|
+
|
|
5
|
+
const TAG = 'idle-window'
|
|
3
6
|
|
|
4
7
|
const DEFAULT_IDLE_THRESHOLD_MS = 120_000 // 2 minutes
|
|
5
8
|
const DAILY_GUARANTEE_MS = 24 * 60 * 60 * 1000
|
|
@@ -73,7 +76,7 @@ export async function drainIdleWindowCallbacks(): Promise<void> {
|
|
|
73
76
|
try {
|
|
74
77
|
await cb()
|
|
75
78
|
} catch (err) {
|
|
76
|
-
|
|
79
|
+
log.warn(TAG, 'Callback failed:', err instanceof Error ? err.message : String(err))
|
|
77
80
|
}
|
|
78
81
|
}
|
|
79
82
|
}
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { hmrSingleton } from '@/lib/shared-utils'
|
|
18
|
+
import { log } from '@/lib/server/logger'
|
|
19
|
+
|
|
20
|
+
const TAG = 'perf'
|
|
18
21
|
|
|
19
22
|
interface PerfEntry {
|
|
20
23
|
category: string
|
|
@@ -44,7 +47,7 @@ function emitEntry(entry: PerfEntry): void {
|
|
|
44
47
|
const metaStr = entry.meta && Object.keys(entry.meta).length > 0
|
|
45
48
|
? ' ' + JSON.stringify(entry.meta)
|
|
46
49
|
: ''
|
|
47
|
-
|
|
50
|
+
log.info(TAG, `${entry.category}/${entry.label} ${entry.durationMs}ms${metaStr}`)
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
const _noopEnd = () => 0
|
|
@@ -2,6 +2,9 @@ import { genId } from '@/lib/id'
|
|
|
2
2
|
import { hmrSingleton, sleep } from '@/lib/shared-utils'
|
|
3
3
|
import { spawn, type ChildProcessWithoutNullStreams } from 'child_process'
|
|
4
4
|
import { detectDocker } from '@/lib/server/sandbox/docker-detect'
|
|
5
|
+
import { log } from '@/lib/server/logger'
|
|
6
|
+
|
|
7
|
+
const TAG = 'process-manager'
|
|
5
8
|
|
|
6
9
|
const MAX_LOG_CHARS = 200_000
|
|
7
10
|
const DEFAULT_BACKGROUND_YIELD_MS = 10_000
|
|
@@ -127,9 +130,9 @@ function cleanupSandboxContainer(containerName: string) {
|
|
|
127
130
|
if (!detectDocker().available) return
|
|
128
131
|
try {
|
|
129
132
|
const child = spawn('docker', ['rm', '-f', containerName], { stdio: 'ignore', detached: true })
|
|
130
|
-
child.on('error', (err) => {
|
|
133
|
+
child.on('error', (err) => { log.warn(TAG, `Docker cleanup error for ${containerName}:`, err.message) })
|
|
131
134
|
child.unref()
|
|
132
|
-
} catch (err: unknown) {
|
|
135
|
+
} catch (err: unknown) { log.warn(TAG, `Docker cleanup spawn failed for ${containerName}:`, err instanceof Error ? err.message : String(err)) }
|
|
133
136
|
}
|
|
134
137
|
|
|
135
138
|
function normalizeLines(text: string): string[] {
|
|
@@ -264,7 +267,7 @@ export async function startManagedProcess(opts: StartProcessOptions): Promise<St
|
|
|
264
267
|
try { child.kill('SIGTERM') } catch { /* noop */ }
|
|
265
268
|
const escalationTimer = setTimeout(() => {
|
|
266
269
|
if (!state.children.has(id)) return
|
|
267
|
-
|
|
270
|
+
log.warn(TAG, `Process ${id} (pid=${rec.pid}) did not exit after SIGTERM, sending SIGKILL`)
|
|
268
271
|
appendLog(id, '\n[process] SIGKILL escalation — process ignored SIGTERM.\n')
|
|
269
272
|
try { child.kill('SIGKILL') } catch { /* noop */ }
|
|
270
273
|
}, SIGKILL_ESCALATION_MS)
|
|
@@ -491,7 +494,7 @@ export async function reapOrphanedSandboxContainers(): Promise<number> {
|
|
|
491
494
|
(r) => r.sandboxContainerName === name && r.status === 'running',
|
|
492
495
|
)
|
|
493
496
|
if (!isTracked) {
|
|
494
|
-
|
|
497
|
+
log.warn(TAG, `Reaping orphaned sandbox container: ${name}`)
|
|
495
498
|
cleanupSandboxContainer(name)
|
|
496
499
|
reaped++
|
|
497
500
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { log } from '@/lib/server/logger'
|
|
1
2
|
import { matchesCapabilities, filterAgentsByCapabilities, capabilityMatchScore } from '@/lib/server/agents/capability-match'
|
|
2
3
|
import { genId } from '@/lib/id'
|
|
3
4
|
import { dedup, hmrSingleton, jitteredBackoff } from '@/lib/shared-utils'
|
|
@@ -44,6 +45,8 @@ import {
|
|
|
44
45
|
} from '@/lib/server/tasks/task-lifecycle'
|
|
45
46
|
import { noteMissionTaskFinished, noteMissionTaskStarted } from '@/lib/server/missions/mission-service'
|
|
46
47
|
|
|
48
|
+
const TAG = 'queue'
|
|
49
|
+
|
|
47
50
|
export const collectTaskConnectorFollowupTargets = collectTaskConnectorFollowupTargetsImpl
|
|
48
51
|
export const resolveTaskOriginConnectorFollowupTarget = resolveTaskOriginConnectorFollowupTargetImpl
|
|
49
52
|
|
|
@@ -649,7 +652,7 @@ function queueTaskAutonomyObservation(input: {
|
|
|
649
652
|
toolEvents: input.toolEvents,
|
|
650
653
|
sourceMessage: input.sourceMessage,
|
|
651
654
|
}).catch((err: unknown) => {
|
|
652
|
-
|
|
655
|
+
log.warn(TAG, `[queue] Autonomy observation failed for ${input.runId}:`, err)
|
|
653
656
|
})
|
|
654
657
|
}
|
|
655
658
|
|
|
@@ -739,7 +742,7 @@ async function executeTaskRun(
|
|
|
739
742
|
try {
|
|
740
743
|
const followupBudget = checkAgentBudgetLimits(typedAgentForBudget)
|
|
741
744
|
if (!followupBudget.ok) {
|
|
742
|
-
|
|
745
|
+
log.warn(TAG, `[queue] Budget exceeded for "${typedAgentForBudget.name}" during follow-up, stopping.`)
|
|
743
746
|
break
|
|
744
747
|
}
|
|
745
748
|
} catch {}
|
|
@@ -967,7 +970,7 @@ export function disableSessionHeartbeat(sessionId: string | null | undefined) {
|
|
|
967
970
|
session.heartbeatEnabled = false
|
|
968
971
|
session.lastActiveAt = Date.now()
|
|
969
972
|
saveSessions(sessions)
|
|
970
|
-
|
|
973
|
+
log.info(TAG, `[queue] Disabled heartbeat on session ${sessionId} (task finished)`)
|
|
971
974
|
}
|
|
972
975
|
|
|
973
976
|
export function enqueueTask(taskId: string) {
|
|
@@ -1062,7 +1065,7 @@ export function validateCompletedTasksQueue() {
|
|
|
1062
1065
|
if (tasksDirty) { saveTasks(tasks); notify('tasks') }
|
|
1063
1066
|
if (sessionsDirty) saveSessions(sessions)
|
|
1064
1067
|
if (demoted > 0) {
|
|
1065
|
-
|
|
1068
|
+
log.warn(TAG, `[queue] Demoted ${demoted} invalid completed task(s) to failed after validation audit`)
|
|
1066
1069
|
}
|
|
1067
1070
|
return { checked, demoted }
|
|
1068
1071
|
}
|
|
@@ -1208,7 +1211,7 @@ export async function processNext() {
|
|
|
1208
1211
|
let recovered = false
|
|
1209
1212
|
for (const [id, t] of Object.entries(allTasks) as [string, BoardTask][]) {
|
|
1210
1213
|
if (t.status === 'queued' && !queueSet.has(id)) {
|
|
1211
|
-
|
|
1214
|
+
log.info(TAG, `[queue] Recovering orphaned queued task: "${t.title}" (${id})`)
|
|
1212
1215
|
pushQueueUnique(currentQueue, id)
|
|
1213
1216
|
recovered = true
|
|
1214
1217
|
}
|
|
@@ -1243,7 +1246,7 @@ export async function processNext() {
|
|
|
1243
1246
|
// Put it back in the queue and skip
|
|
1244
1247
|
pushQueueUnique(queue, taskId)
|
|
1245
1248
|
saveQueue(queue)
|
|
1246
|
-
|
|
1249
|
+
log.info(TAG, `[queue] Skipping task "${task.title}" (${taskId}) — blocked by incomplete dependencies`)
|
|
1247
1250
|
return
|
|
1248
1251
|
}
|
|
1249
1252
|
}
|
|
@@ -1277,7 +1280,7 @@ export async function processNext() {
|
|
|
1277
1280
|
return a.name.localeCompare(b.name)
|
|
1278
1281
|
})
|
|
1279
1282
|
const rerouted = candidates[0]
|
|
1280
|
-
|
|
1283
|
+
log.info(TAG, `[queue] Rerouting task "${task.title}" (${taskId}) from agent "${agent.name}" to "${rerouted.name}" — capability match`)
|
|
1281
1284
|
task.agentId = rerouted.id
|
|
1282
1285
|
agent = rerouted
|
|
1283
1286
|
} else {
|
|
@@ -1473,7 +1476,7 @@ export async function processNext() {
|
|
|
1473
1476
|
saveSessions(sessions)
|
|
1474
1477
|
}
|
|
1475
1478
|
|
|
1476
|
-
|
|
1479
|
+
log.info(TAG, `[queue] Running task "${task.title}" (${taskId}) with ${agent.name}`)
|
|
1477
1480
|
|
|
1478
1481
|
try {
|
|
1479
1482
|
const taskRunId = `${taskId}:attempt-${(task.attempts || 0) + 1}`
|
|
@@ -1508,7 +1511,7 @@ export async function processNext() {
|
|
|
1508
1511
|
toolEvents: taskRun.toolEvents,
|
|
1509
1512
|
sourceMessage: task.description || task.title,
|
|
1510
1513
|
})
|
|
1511
|
-
|
|
1514
|
+
log.warn(TAG, `[queue] Task "${task.title}" cancelled during execution`)
|
|
1512
1515
|
return
|
|
1513
1516
|
}
|
|
1514
1517
|
if (t2[taskId]) {
|
|
@@ -1596,10 +1599,10 @@ export async function processNext() {
|
|
|
1596
1599
|
else if (opencodeId) t2[taskId].cliProvider = 'opencode-cli'
|
|
1597
1600
|
else if (geminiId) t2[taskId].cliProvider = 'gemini-cli'
|
|
1598
1601
|
}
|
|
1599
|
-
|
|
1602
|
+
log.info(TAG, `[queue] CLI resume IDs for task ${taskId}: claude=${claudeId}, codex=${codexId}, opencode=${opencodeId}, gemini=${geminiId}`)
|
|
1600
1603
|
}
|
|
1601
1604
|
} catch (e) {
|
|
1602
|
-
|
|
1605
|
+
log.warn(TAG, `[queue] Failed to extract CLI resume IDs for task ${taskId}:`, e)
|
|
1603
1606
|
}
|
|
1604
1607
|
|
|
1605
1608
|
saveTasks(t2)
|
|
@@ -1640,7 +1643,7 @@ export async function processNext() {
|
|
|
1640
1643
|
cleanupTerminalOneOffSchedule(doneTask)
|
|
1641
1644
|
// Clean up LangGraph checkpoints for completed tasks
|
|
1642
1645
|
getCheckpointSaver().deleteThread(taskId).catch((e) =>
|
|
1643
|
-
|
|
1646
|
+
log.warn(TAG, `[queue] Failed to clean up checkpoints for task ${taskId}:`, e)
|
|
1644
1647
|
)
|
|
1645
1648
|
// Cascade unblock: auto-queue tasks whose blockers are all done
|
|
1646
1649
|
const latestTasks = loadTasks()
|
|
@@ -1649,7 +1652,7 @@ export async function processNext() {
|
|
|
1649
1652
|
saveTasks(latestTasks)
|
|
1650
1653
|
for (const uid of unblockedIds) {
|
|
1651
1654
|
enqueueTask(uid)
|
|
1652
|
-
|
|
1655
|
+
log.info(TAG, `[queue] Auto-unblocked task "${latestTasks[uid]?.title}" (${uid})`)
|
|
1653
1656
|
}
|
|
1654
1657
|
notify('tasks')
|
|
1655
1658
|
}
|
|
@@ -1659,15 +1662,15 @@ export async function processNext() {
|
|
|
1659
1662
|
const { wakeProtocolRunFromTaskCompletion } = await import('@/lib/server/protocols/protocol-service')
|
|
1660
1663
|
wakeProtocolRunFromTaskCompletion(taskId)
|
|
1661
1664
|
} catch (e) {
|
|
1662
|
-
|
|
1665
|
+
log.warn(TAG, `[queue] Failed to wake protocol run for task ${taskId}:`, e)
|
|
1663
1666
|
}
|
|
1664
1667
|
}
|
|
1665
|
-
|
|
1668
|
+
log.info(TAG, `[queue] Task "${task.title}" completed`)
|
|
1666
1669
|
} else if (doneTask?.status === 'cancelled') {
|
|
1667
|
-
|
|
1670
|
+
log.warn(TAG, `[queue] Task "${task.title}" cancelled during execution`)
|
|
1668
1671
|
} else {
|
|
1669
1672
|
if (doneTask?.status === 'queued') {
|
|
1670
|
-
|
|
1673
|
+
log.warn(TAG, `[queue] Task "${task.title}" scheduled for retry`)
|
|
1671
1674
|
} else {
|
|
1672
1675
|
pushMainLoopEventToMainSessions({
|
|
1673
1676
|
type: 'task_failed',
|
|
@@ -1678,12 +1681,12 @@ export async function processNext() {
|
|
|
1678
1681
|
handleTerminalTaskResultDeliveries(doneTask)
|
|
1679
1682
|
cleanupTerminalOneOffSchedule(doneTask)
|
|
1680
1683
|
}
|
|
1681
|
-
|
|
1684
|
+
log.warn(TAG, `[queue] Task "${task.title}" failed completion validation`)
|
|
1682
1685
|
}
|
|
1683
1686
|
}
|
|
1684
1687
|
} catch (err: unknown) {
|
|
1685
1688
|
const errMsg = err instanceof Error ? err.message : String(err || 'Unknown error')
|
|
1686
|
-
|
|
1689
|
+
log.error(TAG, `[queue] Task "${task.title}" failed:`, errMsg)
|
|
1687
1690
|
const taskRunId = `${taskId}:attempt-${(task.attempts || 0) + 1}`
|
|
1688
1691
|
const t2 = loadTasks()
|
|
1689
1692
|
if (isCancelledTask(t2[taskId])) {
|
|
@@ -1699,7 +1702,7 @@ export async function processNext() {
|
|
|
1699
1702
|
error: t2[taskId].error || errMsg,
|
|
1700
1703
|
sourceMessage: task.description || task.title,
|
|
1701
1704
|
})
|
|
1702
|
-
|
|
1705
|
+
log.warn(TAG, `[queue] Task "${task.title}" aborted because it was cancelled`)
|
|
1703
1706
|
return
|
|
1704
1707
|
}
|
|
1705
1708
|
if (t2[taskId]) {
|
|
@@ -1720,9 +1723,9 @@ export async function processNext() {
|
|
|
1720
1723
|
source: 'task-repair',
|
|
1721
1724
|
runId: repairRunId,
|
|
1722
1725
|
})
|
|
1723
|
-
|
|
1726
|
+
log.info(TAG, `[queue] Repair turn completed for task "${task.title}" (${taskId})`)
|
|
1724
1727
|
} catch (repairErr: unknown) {
|
|
1725
|
-
|
|
1728
|
+
log.warn(TAG, `[queue] Repair turn failed for task "${task.title}":`, repairErr instanceof Error ? repairErr.message : String(repairErr))
|
|
1726
1729
|
// If repair fails, attempt guardian recovery
|
|
1727
1730
|
const taskCwd = t2[taskId].cwd || WORKSPACE_DIR
|
|
1728
1731
|
prepareGuardianRecovery({
|
|
@@ -1784,9 +1787,9 @@ export async function processNext() {
|
|
|
1784
1787
|
})
|
|
1785
1788
|
const latest = loadTasks()[taskId] as BoardTask | undefined
|
|
1786
1789
|
if (latest?.status === 'queued') {
|
|
1787
|
-
|
|
1790
|
+
log.warn(TAG, `[queue] Task "${task.title}" queued for retry after error`)
|
|
1788
1791
|
} else if (latest?.status === 'cancelled') {
|
|
1789
|
-
|
|
1792
|
+
log.warn(TAG, `[queue] Task "${task.title}" stayed cancelled after abort`)
|
|
1790
1793
|
} else {
|
|
1791
1794
|
pushMainLoopEventToMainSessions({
|
|
1792
1795
|
type: 'task_failed',
|
|
@@ -1828,7 +1831,7 @@ export function cleanupFinishedTaskSessions() {
|
|
|
1828
1831
|
}
|
|
1829
1832
|
if (cleaned > 0) {
|
|
1830
1833
|
saveSessions(sessions)
|
|
1831
|
-
|
|
1834
|
+
log.info(TAG, `[queue] Disabled heartbeat on ${cleaned} session(s) with finished tasks`)
|
|
1832
1835
|
}
|
|
1833
1836
|
}
|
|
1834
1837
|
|
|
@@ -1976,7 +1979,7 @@ export function resumeQueue() {
|
|
|
1976
1979
|
for (const task of Object.values(tasks) as BoardTask[]) {
|
|
1977
1980
|
if (task.status === 'queued' && !queue.includes(task.id)) {
|
|
1978
1981
|
applyTaskPolicyDefaults(task)
|
|
1979
|
-
|
|
1982
|
+
log.info(TAG, `[queue] Recovering stuck queued task: "${task.title}" (${task.id})`)
|
|
1980
1983
|
queue.push(task.id)
|
|
1981
1984
|
task.queuedAt = task.queuedAt || Date.now()
|
|
1982
1985
|
modified = true
|
|
@@ -2004,7 +2007,7 @@ export function resumeQueue() {
|
|
|
2004
2007
|
recovered++
|
|
2005
2008
|
}
|
|
2006
2009
|
if (recovered > 0) {
|
|
2007
|
-
|
|
2010
|
+
log.info(TAG, `[queue] Recovered ${recovered} orphaned running task(s) on boot`)
|
|
2008
2011
|
}
|
|
2009
2012
|
|
|
2010
2013
|
if (modified) {
|
|
@@ -2013,7 +2016,7 @@ export function resumeQueue() {
|
|
|
2013
2016
|
}
|
|
2014
2017
|
|
|
2015
2018
|
if (queue.length > 0) {
|
|
2016
|
-
|
|
2019
|
+
log.info(TAG, `[queue] Resuming ${queue.length} queued task(s) on boot`)
|
|
2017
2020
|
processNext()
|
|
2018
2021
|
}
|
|
2019
2022
|
}
|
|
@@ -11,8 +11,11 @@ import { ensureAgentThreadSession } from '@/lib/server/agents/agent-thread-sessi
|
|
|
11
11
|
import { ensureMissionForTask, noteScheduleMissionTriggered } from '@/lib/server/missions/mission-service'
|
|
12
12
|
import { hasActiveProtocolRunForSchedule, launchProtocolRunForSchedule } from '@/lib/server/protocols/protocol-service'
|
|
13
13
|
import { hmrSingleton } from '@/lib/shared-utils'
|
|
14
|
+
import { log } from '@/lib/server/logger'
|
|
14
15
|
import type { Schedule } from '@/types'
|
|
15
16
|
|
|
17
|
+
const TAG = 'scheduler'
|
|
18
|
+
|
|
16
19
|
const TICK_INTERVAL = 60_000 // 60 seconds
|
|
17
20
|
const schedulerState = hmrSingleton('__swarmclaw_scheduler_state__', () => ({
|
|
18
21
|
intervalId: null as ReturnType<typeof setInterval> | null,
|
|
@@ -45,7 +48,7 @@ function shouldLaunchScheduleProtocol(schedule: Schedule): boolean {
|
|
|
45
48
|
|
|
46
49
|
export function startScheduler() {
|
|
47
50
|
if (schedulerState.intervalId) return
|
|
48
|
-
|
|
51
|
+
log.info(TAG, 'Starting scheduler engine (60s tick)')
|
|
49
52
|
|
|
50
53
|
// Compute initial nextRunAt for cron schedules missing it
|
|
51
54
|
computeNextRuns()
|
|
@@ -57,7 +60,7 @@ export function stopScheduler() {
|
|
|
57
60
|
if (schedulerState.intervalId) {
|
|
58
61
|
clearInterval(schedulerState.intervalId)
|
|
59
62
|
schedulerState.intervalId = null
|
|
60
|
-
|
|
63
|
+
log.info(TAG, 'Stopped scheduler engine')
|
|
61
64
|
}
|
|
62
65
|
}
|
|
63
66
|
|
|
@@ -75,7 +78,7 @@ function computeNextRuns() {
|
|
|
75
78
|
schedule.nextRunAt = interval.next().getTime()
|
|
76
79
|
changedEntries.push([schedule.id, schedule])
|
|
77
80
|
} catch (err) {
|
|
78
|
-
|
|
81
|
+
log.error(TAG, `Invalid cron for ${schedule.id}:`, err)
|
|
79
82
|
schedule.status = 'failed'
|
|
80
83
|
changedEntries.push([schedule.id, schedule])
|
|
81
84
|
}
|
|
@@ -133,7 +136,7 @@ async function tick(now = Date.now()) {
|
|
|
133
136
|
|
|
134
137
|
const agent = agents[schedule.agentId]
|
|
135
138
|
if (!agent) {
|
|
136
|
-
|
|
139
|
+
log.error(TAG, `Agent ${schedule.agentId} not found for schedule ${schedule.id}`)
|
|
137
140
|
schedule.status = 'failed'
|
|
138
141
|
upsertSchedule(schedule.id, schedule)
|
|
139
142
|
pushMainLoopEventToMainSessions({
|
|
@@ -143,7 +146,7 @@ async function tick(now = Date.now()) {
|
|
|
143
146
|
continue
|
|
144
147
|
}
|
|
145
148
|
if (isAgentDisabled(agent)) {
|
|
146
|
-
|
|
149
|
+
log.warn(TAG, `Skipping schedule "${schedule.name}" (${schedule.id}) because agent ${schedule.agentId} is disabled`)
|
|
147
150
|
advanceSchedule(schedule)
|
|
148
151
|
upsertSchedule(schedule.id, schedule)
|
|
149
152
|
pushMainLoopEventToMainSessions({
|
|
@@ -153,7 +156,7 @@ async function tick(now = Date.now()) {
|
|
|
153
156
|
continue
|
|
154
157
|
}
|
|
155
158
|
|
|
156
|
-
|
|
159
|
+
log.info(TAG, `Firing schedule "${schedule.name}" (${schedule.id})`)
|
|
157
160
|
schedule.lastRunAt = now
|
|
158
161
|
schedule.runNumber = (schedule.runNumber || 0) + 1
|
|
159
162
|
// Compute next run
|
|
@@ -818,6 +818,9 @@ async function drainExecution(executionKey: string): Promise<void> {
|
|
|
818
818
|
next.run.missionId = result.missionId || next.run.missionId || null
|
|
819
819
|
finishedMissionId = next.run.missionId || null
|
|
820
820
|
next.run.resultPreview = result.text?.slice(0, 280)
|
|
821
|
+
if (typeof result.inputTokens === 'number') next.run.totalInputTokens = result.inputTokens
|
|
822
|
+
if (typeof result.outputTokens === 'number') next.run.totalOutputTokens = result.outputTokens
|
|
823
|
+
if (typeof result.estimatedCost === 'number') next.run.estimatedCost = result.estimatedCost
|
|
821
824
|
syncRunRecord(next.run)
|
|
822
825
|
emitRunMeta(next, next.run.status, {
|
|
823
826
|
persisted: result.persisted,
|
|
@@ -3,10 +3,16 @@ type BridgeAuth = {
|
|
|
3
3
|
password?: string
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
+
const AUTH_BY_PORT_MAX = 200
|
|
6
7
|
const authByPort = new Map<number, BridgeAuth>()
|
|
7
8
|
|
|
8
9
|
export function setBridgeAuthForPort(port: number, auth: BridgeAuth): void {
|
|
9
10
|
if (!Number.isFinite(port) || port <= 0) return
|
|
11
|
+
// FIFO eviction at cap
|
|
12
|
+
if (!authByPort.has(port) && authByPort.size >= AUTH_BY_PORT_MAX) {
|
|
13
|
+
const firstKey = authByPort.keys().next().value
|
|
14
|
+
if (firstKey !== undefined) authByPort.delete(firstKey)
|
|
15
|
+
}
|
|
10
16
|
const token = typeof auth.token === 'string' ? auth.token.trim() : ''
|
|
11
17
|
const password = typeof auth.password === 'string' ? auth.password.trim() : ''
|
|
12
18
|
authByPort.set(port, {
|
|
@@ -15,12 +15,22 @@ export type NoVncObserverTokenPayload = {
|
|
|
15
15
|
password?: string
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
const NOVNC_TOKEN_MAX = 500
|
|
18
19
|
const noVncObserverTokens = new Map<string, NoVncObserverTokenEntry>()
|
|
19
20
|
|
|
20
21
|
function pruneExpiredObserverTokens(now: number): void {
|
|
21
22
|
for (const [token, entry] of noVncObserverTokens) {
|
|
22
23
|
if (entry.expiresAt <= now) noVncObserverTokens.delete(token)
|
|
23
24
|
}
|
|
25
|
+
// Hard cap as safety net
|
|
26
|
+
if (noVncObserverTokens.size > NOVNC_TOKEN_MAX) {
|
|
27
|
+
const excess = noVncObserverTokens.size - NOVNC_TOKEN_MAX
|
|
28
|
+
const iter = noVncObserverTokens.keys()
|
|
29
|
+
for (let i = 0; i < excess; i++) {
|
|
30
|
+
const k = iter.next().value
|
|
31
|
+
if (k !== undefined) noVncObserverTokens.delete(k)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
24
34
|
}
|
|
25
35
|
|
|
26
36
|
export function isNoVncEnabled(params: { enableNoVnc: boolean; headless: boolean }): boolean {
|
|
@@ -164,6 +164,7 @@ export function extractResumeIdentifier(text: string): string | null {
|
|
|
164
164
|
return null
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
const BINARY_LOOKUP_CACHE_MAX = 100
|
|
167
168
|
const binaryLookupCache = new Map<string, { checkedAt: number; path: string | null }>()
|
|
168
169
|
const BINARY_LOOKUP_TTL_MS = 30_000
|
|
169
170
|
const isWindows = process.platform === 'win32'
|
|
@@ -173,6 +174,19 @@ export function findBinaryOnPath(binaryName: string): string | null {
|
|
|
173
174
|
const cached = binaryLookupCache.get(binaryName)
|
|
174
175
|
if (cached && now - cached.checkedAt < BINARY_LOOKUP_TTL_MS) return cached.path
|
|
175
176
|
|
|
177
|
+
// Prune expired + cap
|
|
178
|
+
for (const [k, v] of binaryLookupCache) {
|
|
179
|
+
if (now - v.checkedAt > BINARY_LOOKUP_TTL_MS) binaryLookupCache.delete(k)
|
|
180
|
+
}
|
|
181
|
+
if (binaryLookupCache.size > BINARY_LOOKUP_CACHE_MAX) {
|
|
182
|
+
const excess = binaryLookupCache.size - BINARY_LOOKUP_CACHE_MAX
|
|
183
|
+
const iter = binaryLookupCache.keys()
|
|
184
|
+
for (let i = 0; i < excess; i++) {
|
|
185
|
+
const k = iter.next().value
|
|
186
|
+
if (k !== undefined) binaryLookupCache.delete(k)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
176
190
|
const { spawnSync } = require('child_process')
|
|
177
191
|
const probe = isWindows
|
|
178
192
|
? spawnSync('where', [binaryName], { encoding: 'utf-8', timeout: 2000, stdio: 'pipe' })
|
|
@@ -11,6 +11,9 @@ import { loadSessions, patchAgent, patchSession } from '../storage'
|
|
|
11
11
|
import { inferExtensionPublisherSourceFromUrl } from '@/lib/extension-sources'
|
|
12
12
|
import { errorMessage } from '@/lib/shared-utils'
|
|
13
13
|
import { getEnabledCapabilityIds, isExternalExtensionId, normalizeCapabilitySelection } from '@/lib/capability-selection'
|
|
14
|
+
import { log } from '@/lib/server/logger'
|
|
15
|
+
|
|
16
|
+
const TAG = 'session-tools-discovery'
|
|
14
17
|
|
|
15
18
|
function grantCapabilitySelection(current: {
|
|
16
19
|
tools?: string[] | null
|
|
@@ -62,7 +65,7 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
|
|
|
62
65
|
const q = typeof normalized.query === 'string' ? normalized.query : ''
|
|
63
66
|
const extensionId = explicitExtensionId || (action === 'request_access' ? q.trim() : '')
|
|
64
67
|
|
|
65
|
-
|
|
68
|
+
log.info(TAG, 'Executing action:', { action, query: q, extensionId })
|
|
66
69
|
|
|
67
70
|
try {
|
|
68
71
|
switch (action) {
|
|
@@ -87,7 +90,7 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
|
|
|
87
90
|
const results: Record<string, unknown>[] = []
|
|
88
91
|
|
|
89
92
|
try {
|
|
90
|
-
|
|
93
|
+
log.info(TAG, 'Searching ClawHub...')
|
|
91
94
|
const hubResults = await searchClawHub(q)
|
|
92
95
|
if (hubResults && hubResults.skills) {
|
|
93
96
|
results.push(...hubResults.skills.map((s) => ({
|
|
@@ -101,11 +104,11 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
|
|
|
101
104
|
})))
|
|
102
105
|
}
|
|
103
106
|
} catch (err: unknown) {
|
|
104
|
-
|
|
107
|
+
log.error(TAG, 'ClawHub search failed:', errorMessage(err))
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
try {
|
|
108
|
-
|
|
111
|
+
log.info(TAG, 'Searching SwarmClaw registry...')
|
|
109
112
|
const registryResults = new Map<string, Record<string, unknown>>()
|
|
110
113
|
const registries = [
|
|
111
114
|
{ url: 'https://swarmclaw.ai/registry/extensions.json', catalogSource: 'swarmclaw-site' },
|
|
@@ -133,7 +136,7 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
|
|
|
133
136
|
}
|
|
134
137
|
results.push(...registryResults.values())
|
|
135
138
|
} catch (err: unknown) {
|
|
136
|
-
|
|
139
|
+
log.error(TAG, 'SC Registry search failed:', errorMessage(err))
|
|
137
140
|
}
|
|
138
141
|
|
|
139
142
|
if (results.length === 0) {
|
|
@@ -226,7 +229,7 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
|
|
|
226
229
|
}
|
|
227
230
|
} catch (err: unknown) {
|
|
228
231
|
const msg = errorMessage(err)
|
|
229
|
-
|
|
232
|
+
log.error(TAG, 'executeDiscoveryAction failed:', msg)
|
|
230
233
|
return `Error: ${msg}`
|
|
231
234
|
}
|
|
232
235
|
}
|
|
@@ -57,6 +57,8 @@ import {
|
|
|
57
57
|
export type { ToolContext, SessionToolsResult }
|
|
58
58
|
export { sweepOrphanedBrowsers, cleanupSessionBrowser, getActiveBrowserCount, hasActiveBrowser }
|
|
59
59
|
|
|
60
|
+
const TAG = 'session-tools'
|
|
61
|
+
|
|
60
62
|
const DELEGATION_TOOL_NAMES = new Set([
|
|
61
63
|
'delegate',
|
|
62
64
|
'spawn_subagent',
|
|
@@ -455,7 +457,7 @@ export async function buildSessionTools(cwd: string, enabledExtensions: string[]
|
|
|
455
457
|
abortSignalRef,
|
|
456
458
|
}
|
|
457
459
|
} catch (err: any) {
|
|
458
|
-
|
|
460
|
+
log.error(TAG, 'buildSessionTools critical failure:', err.message)
|
|
459
461
|
throw err
|
|
460
462
|
}
|
|
461
463
|
}
|
|
@@ -226,7 +226,7 @@ const PlatformExtension: Extension = {
|
|
|
226
226
|
description: 'Unified management of agents, projects, tasks, schedules, skills, documents, and secrets.',
|
|
227
227
|
hooks: {
|
|
228
228
|
getCapabilityDescription: () => 'I can manage durable execution context across agents, projects, tasks, schedules, documents, skills, webhooks, connectors, sessions, and encrypted secrets.',
|
|
229
|
-
getOperatingGuidance: () => ['Use projects to hold longer-lived goals, objectives, and credential requirements.', 'Create/update tasks for long-lived goals to track progress.', 'Use schedules for follow-ups and heartbeat-style check-ins. Check existing schedules before creating new ones.', 'Inspect existing chats before creating duplicates.'],
|
|
229
|
+
getOperatingGuidance: () => ['Use projects to hold longer-lived goals, objectives, and credential requirements.', 'Create/update tasks for long-lived goals to track progress.', 'When work on a task is finished, update its status to completed with a concrete result summary. Cancel or archive tasks that are no longer relevant. Stale open tasks block project progress.', 'Use schedules for follow-ups and heartbeat-style check-ins. Check existing schedules before creating new ones.', 'Inspect existing chats before creating duplicates.'],
|
|
230
230
|
} as ExtensionHooks,
|
|
231
231
|
tools: [
|
|
232
232
|
{
|
|
@@ -5,6 +5,7 @@ import type { Extension, ExtensionHooks } from '@/types'
|
|
|
5
5
|
import { registerNativeCapability } from '../native-capabilities'
|
|
6
6
|
import { normalizeToolInputArgs } from './normalize-tool-args'
|
|
7
7
|
import { errorMessage, sleep } from '@/lib/shared-utils'
|
|
8
|
+
import { loadAgents } from '@/lib/server/storage'
|
|
8
9
|
import {
|
|
9
10
|
cancelDelegationJob,
|
|
10
11
|
getDelegationJob,
|
|
@@ -99,7 +100,12 @@ function validateAllowedSubagentTarget(agentId: string, ctx: ActionContext): str
|
|
|
99
100
|
? ctx.delegationTargetAgentIds.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0)
|
|
100
101
|
: []
|
|
101
102
|
if (allowedAgentIds.length === 0 || allowedAgentIds.includes(agentId)) return null
|
|
102
|
-
|
|
103
|
+
|
|
104
|
+
const agents = loadAgents()
|
|
105
|
+
const allowedNames = allowedAgentIds
|
|
106
|
+
.map(id => agents[id]?.name ? `${agents[id].name} [${id}]` : id)
|
|
107
|
+
.join(', ')
|
|
108
|
+
return `Error: agent "${agentId}" is not in your allowed delegation list. You may only delegate to: ${allowedNames}. Do not retry with this agent.`
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
function parseBooleanLike(value: unknown): boolean | unknown {
|
|
@@ -538,6 +544,21 @@ registerNativeCapability('subagent', SubagentExtension)
|
|
|
538
544
|
*/
|
|
539
545
|
export function buildSubagentTools(bctx: ToolBuildContext): StructuredToolInterface[] {
|
|
540
546
|
if (!bctx.ctx?.delegationEnabled || !bctx.hasExtension('spawn_subagent')) return []
|
|
547
|
+
|
|
548
|
+
let description = SubagentExtension.tools![0].description
|
|
549
|
+
if (bctx.ctx?.delegationTargetMode === 'selected') {
|
|
550
|
+
const allowedIds = (bctx.ctx.delegationTargetAgentIds || []).filter(
|
|
551
|
+
(id): id is string => typeof id === 'string' && id.trim().length > 0,
|
|
552
|
+
)
|
|
553
|
+
if (allowedIds.length > 0) {
|
|
554
|
+
const agents = loadAgents()
|
|
555
|
+
const allowedSummary = allowedIds
|
|
556
|
+
.map(id => agents[id]?.name ? `${agents[id].name} [${id}]` : id)
|
|
557
|
+
.join(', ')
|
|
558
|
+
description += ` DELEGATION RESTRICTED: You may ONLY delegate to these agents: ${allowedSummary}. Attempts to delegate to any other agent will be rejected.`
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
541
562
|
return [
|
|
542
563
|
tool(
|
|
543
564
|
async (args) => executeSubagentAction(args, {
|
|
@@ -548,7 +569,7 @@ export function buildSubagentTools(bctx: ToolBuildContext): StructuredToolInterf
|
|
|
548
569
|
}),
|
|
549
570
|
{
|
|
550
571
|
name: 'spawn_subagent',
|
|
551
|
-
description
|
|
572
|
+
description,
|
|
552
573
|
schema: subagentToolSchema
|
|
553
574
|
}
|
|
554
575
|
)
|
|
@@ -39,6 +39,9 @@ import {
|
|
|
39
39
|
simulateSolanaTransaction,
|
|
40
40
|
} from '../solana'
|
|
41
41
|
import { TOOL_CAPABILITY } from '../tool-planning'
|
|
42
|
+
import { log } from '@/lib/server/logger'
|
|
43
|
+
|
|
44
|
+
const TAG = 'wallet'
|
|
42
45
|
import { clearWalletPortfolioCache } from '@/lib/server/wallet/wallet-portfolio'
|
|
43
46
|
import {
|
|
44
47
|
createAgentWallet,
|
|
@@ -1042,7 +1045,7 @@ async function executeWalletAction(args: unknown, context: { agentId?: string |
|
|
|
1042
1045
|
} catch (err: unknown) {
|
|
1043
1046
|
const msg = errorMessage(err)
|
|
1044
1047
|
if (msg.includes('429') || msg.toLowerCase().includes('too many requests')) {
|
|
1045
|
-
|
|
1048
|
+
log.warn(TAG, 'Solana RPC rate-limited. Consider using a dedicated RPC endpoint (SOLANA_RPC_URL env var).')
|
|
1046
1049
|
}
|
|
1047
1050
|
return JSON.stringify({ error: msg })
|
|
1048
1051
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { ClawHubSkill } from '@/types'
|
|
2
2
|
import { errorMessage } from '@/lib/shared-utils'
|
|
3
|
+
import { log } from '@/lib/server/logger'
|
|
4
|
+
|
|
5
|
+
const TAG = 'clawhub-client'
|
|
3
6
|
|
|
4
7
|
export interface ClawHubSearchResult {
|
|
5
8
|
skills: ClawHubSkill[]
|
|
@@ -177,7 +180,7 @@ export async function searchClawHub(query: string, page = 1, limit = 20, cursor?
|
|
|
177
180
|
return { skills, total, page, nextCursor: data.nextCursor }
|
|
178
181
|
} catch (err: unknown) {
|
|
179
182
|
const error = errorMessage(err)
|
|
180
|
-
|
|
183
|
+
log.warn(TAG, 'search failed:', error)
|
|
181
184
|
return { skills: [], total: 0, page, error }
|
|
182
185
|
}
|
|
183
186
|
}
|