@swarmclawai/swarmclaw 0.5.2 → 0.6.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 +42 -7
- package/bin/swarmclaw.js +76 -16
- package/next.config.ts +11 -1
- package/package.json +4 -2
- package/public/screenshots/agents.png +0 -0
- package/public/screenshots/dashboard.png +0 -0
- package/public/screenshots/providers.png +0 -0
- package/public/screenshots/tasks.png +0 -0
- package/scripts/postinstall.mjs +18 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +410 -0
- package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
- package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
- package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
- package/src/app/api/chatrooms/[id]/route.ts +84 -0
- package/src/app/api/chatrooms/route.ts +50 -0
- package/src/app/api/credentials/route.ts +2 -3
- package/src/app/api/knowledge/[id]/route.ts +13 -2
- package/src/app/api/knowledge/route.ts +8 -1
- package/src/app/api/memory/route.ts +8 -0
- package/src/app/api/notifications/[id]/route.ts +27 -0
- package/src/app/api/notifications/route.ts +68 -0
- package/src/app/api/orchestrator/run/route.ts +1 -1
- package/src/app/api/plugins/install/route.ts +2 -2
- package/src/app/api/search/route.ts +155 -0
- package/src/app/api/sessions/[id]/chat/route.ts +2 -0
- package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
- package/src/app/api/sessions/[id]/fork/route.ts +1 -1
- package/src/app/api/sessions/route.ts +3 -3
- package/src/app/api/settings/route.ts +9 -0
- package/src/app/api/setup/check-provider/route.ts +3 -16
- package/src/app/api/skills/[id]/route.ts +6 -0
- package/src/app/api/skills/route.ts +6 -0
- package/src/app/api/tasks/[id]/route.ts +20 -0
- package/src/app/api/tasks/bulk/route.ts +100 -0
- package/src/app/api/tasks/route.ts +1 -0
- package/src/app/api/usage/route.ts +45 -0
- package/src/app/api/webhooks/[id]/route.ts +15 -1
- package/src/app/globals.css +58 -15
- package/src/app/page.tsx +142 -13
- package/src/cli/index.js +42 -0
- package/src/cli/index.test.js +30 -0
- package/src/cli/spec.js +32 -0
- package/src/components/agents/agent-avatar.tsx +57 -10
- package/src/components/agents/agent-card.tsx +48 -15
- package/src/components/agents/agent-chat-list.tsx +123 -10
- package/src/components/agents/agent-list.tsx +50 -19
- package/src/components/agents/agent-sheet.tsx +56 -63
- package/src/components/auth/access-key-gate.tsx +10 -3
- package/src/components/auth/setup-wizard.tsx +2 -2
- package/src/components/auth/user-picker.tsx +31 -3
- package/src/components/chat/activity-moment.tsx +169 -0
- package/src/components/chat/chat-header.tsx +2 -0
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/file-path-chip.tsx +125 -0
- package/src/components/chat/markdown-utils.ts +9 -0
- package/src/components/chat/message-bubble.tsx +46 -295
- package/src/components/chat/message-list.tsx +50 -1
- package/src/components/chat/streaming-bubble.tsx +36 -46
- package/src/components/chat/suggestions-bar.tsx +1 -1
- package/src/components/chat/thinking-indicator.tsx +72 -10
- package/src/components/chat/tool-call-bubble.tsx +66 -70
- package/src/components/chat/tool-request-banner.tsx +31 -7
- package/src/components/chat/transfer-agent-picker.tsx +63 -0
- package/src/components/chatrooms/agent-hover-card.tsx +124 -0
- package/src/components/chatrooms/chatroom-input.tsx +320 -0
- package/src/components/chatrooms/chatroom-list.tsx +123 -0
- package/src/components/chatrooms/chatroom-message.tsx +427 -0
- package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
- package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
- package/src/components/chatrooms/chatroom-view.tsx +344 -0
- package/src/components/chatrooms/reaction-picker.tsx +273 -0
- package/src/components/connectors/connector-sheet.tsx +34 -47
- package/src/components/home/home-view.tsx +501 -0
- package/src/components/input/chat-input.tsx +79 -41
- package/src/components/knowledge/knowledge-list.tsx +31 -1
- package/src/components/knowledge/knowledge-sheet.tsx +83 -2
- package/src/components/layout/app-layout.tsx +209 -83
- package/src/components/layout/mobile-header.tsx +2 -0
- package/src/components/layout/update-banner.tsx +2 -2
- package/src/components/logs/log-list.tsx +2 -2
- package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
- package/src/components/memory/memory-agent-list.tsx +143 -0
- package/src/components/memory/memory-browser.tsx +205 -0
- package/src/components/memory/memory-card.tsx +34 -7
- package/src/components/memory/memory-detail.tsx +359 -120
- package/src/components/memory/memory-sheet.tsx +157 -23
- package/src/components/plugins/plugin-list.tsx +1 -1
- package/src/components/plugins/plugin-sheet.tsx +1 -1
- package/src/components/projects/project-detail.tsx +509 -0
- package/src/components/projects/project-list.tsx +195 -59
- package/src/components/providers/provider-list.tsx +2 -2
- package/src/components/providers/provider-sheet.tsx +3 -3
- package/src/components/schedules/schedule-card.tsx +3 -2
- package/src/components/schedules/schedule-list.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +25 -25
- package/src/components/secrets/secret-sheet.tsx +47 -24
- package/src/components/secrets/secrets-list.tsx +18 -8
- package/src/components/sessions/new-session-sheet.tsx +33 -65
- package/src/components/sessions/session-card.tsx +45 -14
- package/src/components/sessions/session-list.tsx +35 -18
- package/src/components/shared/agent-picker-list.tsx +90 -0
- package/src/components/shared/agent-switch-dialog.tsx +156 -0
- package/src/components/shared/attachment-chip.tsx +165 -0
- package/src/components/shared/avatar.tsx +10 -1
- package/src/components/shared/check-icon.tsx +12 -0
- package/src/components/shared/confirm-dialog.tsx +1 -1
- package/src/components/shared/empty-state.tsx +32 -0
- package/src/components/shared/file-preview.tsx +34 -0
- package/src/components/shared/form-styles.ts +2 -0
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
- package/src/components/shared/notification-center.tsx +223 -0
- package/src/components/shared/profile-sheet.tsx +115 -0
- package/src/components/shared/reply-quote.tsx +26 -0
- package/src/components/shared/search-dialog.tsx +296 -0
- package/src/components/shared/section-label.tsx +12 -0
- package/src/components/shared/settings/plugin-manager.tsx +1 -1
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/section-secrets.tsx +1 -1
- package/src/components/shared/settings/section-theme.tsx +95 -0
- package/src/components/shared/settings/section-user-preferences.tsx +39 -0
- package/src/components/shared/settings/settings-page.tsx +180 -27
- package/src/components/shared/settings/settings-sheet.tsx +9 -73
- package/src/components/shared/sheet-footer.tsx +33 -0
- package/src/components/skills/skill-list.tsx +61 -30
- package/src/components/skills/skill-sheet.tsx +81 -2
- package/src/components/tasks/task-board.tsx +448 -26
- package/src/components/tasks/task-card.tsx +46 -9
- package/src/components/tasks/task-column.tsx +62 -3
- package/src/components/tasks/task-list.tsx +12 -4
- package/src/components/tasks/task-sheet.tsx +89 -72
- package/src/components/ui/hover-card.tsx +52 -0
- package/src/components/usage/metrics-dashboard.tsx +78 -0
- package/src/components/usage/usage-list.tsx +1 -1
- package/src/components/webhooks/webhook-sheet.tsx +1 -1
- package/src/hooks/use-view-router.ts +69 -19
- package/src/instrumentation.ts +15 -1
- package/src/lib/chat.ts +2 -0
- package/src/lib/cron-human.ts +114 -0
- package/src/lib/memory.ts +3 -0
- package/src/lib/server/chat-execution.ts +24 -4
- package/src/lib/server/connectors/manager.ts +11 -0
- package/src/lib/server/context-manager.ts +225 -13
- package/src/lib/server/create-notification.ts +42 -0
- package/src/lib/server/daemon-state.ts +165 -10
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service.ts +40 -5
- package/src/lib/server/heartbeat-wake.ts +110 -0
- package/src/lib/server/langgraph-checkpoint.ts +1 -0
- package/src/lib/server/memory-consolidation.ts +92 -0
- package/src/lib/server/memory-db.ts +51 -6
- package/src/lib/server/openclaw-gateway.ts +9 -1
- package/src/lib/server/provider-health.ts +125 -0
- package/src/lib/server/queue.ts +5 -4
- package/src/lib/server/scheduler.ts +8 -0
- package/src/lib/server/session-run-manager.ts +4 -0
- package/src/lib/server/session-tools/chatroom.ts +136 -0
- package/src/lib/server/session-tools/context-mgmt.ts +36 -18
- package/src/lib/server/session-tools/index.ts +2 -0
- package/src/lib/server/session-tools/memory.ts +6 -1
- package/src/lib/server/storage.ts +80 -29
- package/src/lib/server/stream-agent-chat.ts +153 -47
- package/src/lib/server/system-events.ts +49 -0
- package/src/lib/server/ws-hub.ts +11 -0
- package/src/lib/soul-suggestions.ts +109 -0
- package/src/lib/tasks.ts +4 -1
- package/src/lib/view-routes.ts +36 -1
- package/src/lib/ws-client.ts +14 -4
- package/src/proxy.ts +79 -2
- package/src/stores/use-app-store.ts +94 -3
- package/src/stores/use-chat-store.ts +48 -3
- package/src/stores/use-chatroom-store.ts +276 -0
- package/src/types/index.ts +69 -2
|
@@ -329,6 +329,7 @@ function normalizeImage(rawImage: unknown, legacyImagePath?: string | null): Mem
|
|
|
329
329
|
function initDb() {
|
|
330
330
|
const db = new Database(DB_PATH)
|
|
331
331
|
db.pragma('journal_mode = WAL')
|
|
332
|
+
db.pragma('busy_timeout = 5000')
|
|
332
333
|
|
|
333
334
|
db.exec(`
|
|
334
335
|
CREATE TABLE IF NOT EXISTS memories (
|
|
@@ -354,10 +355,15 @@ function initDb() {
|
|
|
354
355
|
'linkedMemoryIds TEXT',
|
|
355
356
|
'"references" TEXT',
|
|
356
357
|
'image TEXT',
|
|
358
|
+
'pinned INTEGER DEFAULT 0',
|
|
359
|
+
'sharedWith TEXT',
|
|
357
360
|
]) {
|
|
358
361
|
try { db.exec(`ALTER TABLE memories ADD COLUMN ${col}`) } catch { /* already exists */ }
|
|
359
362
|
}
|
|
360
363
|
|
|
364
|
+
// Partial index for fast pinned-memory lookups
|
|
365
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_memories_pinned ON memories(agentId, updatedAt DESC) WHERE pinned = 1`)
|
|
366
|
+
|
|
361
367
|
// FTS5 virtual table for full-text search
|
|
362
368
|
db.exec(`
|
|
363
369
|
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
@@ -448,14 +454,14 @@ function initDb() {
|
|
|
448
454
|
insert: db.prepare(`
|
|
449
455
|
INSERT INTO memories (
|
|
450
456
|
id, agentId, sessionId, category, title, content, metadata, embedding,
|
|
451
|
-
"references", filePaths, image, imagePath, linkedMemoryIds, createdAt, updatedAt
|
|
457
|
+
"references", filePaths, image, imagePath, linkedMemoryIds, pinned, sharedWith, createdAt, updatedAt
|
|
452
458
|
)
|
|
453
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
459
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
454
460
|
`),
|
|
455
461
|
update: db.prepare(`
|
|
456
462
|
UPDATE memories
|
|
457
463
|
SET agentId=?, sessionId=?, category=?, title=?, content=?, metadata=?, embedding=?,
|
|
458
|
-
"references"=?, filePaths=?, image=?, imagePath=?, linkedMemoryIds=?, updatedAt=?
|
|
464
|
+
"references"=?, filePaths=?, image=?, imagePath=?, linkedMemoryIds=?, pinned=?, sharedWith=?, updatedAt=?
|
|
459
465
|
WHERE id=?
|
|
460
466
|
`),
|
|
461
467
|
delete: db.prepare(`DELETE FROM memories WHERE id=?`),
|
|
@@ -467,6 +473,9 @@ function initDb() {
|
|
|
467
473
|
},
|
|
468
474
|
listAll: db.prepare(`SELECT * FROM memories ORDER BY updatedAt DESC LIMIT ?`),
|
|
469
475
|
listByAgent: db.prepare(`SELECT * FROM memories WHERE agentId=? ORDER BY updatedAt DESC LIMIT ?`),
|
|
476
|
+
listByAgentOrShared: db.prepare(`SELECT * FROM memories WHERE agentId=? OR sharedWith LIKE ? ORDER BY updatedAt DESC LIMIT ?`),
|
|
477
|
+
listPinnedByAgent: db.prepare(`SELECT * FROM memories WHERE pinned = 1 AND agentId = ? ORDER BY updatedAt DESC LIMIT ?`),
|
|
478
|
+
listPinnedAll: db.prepare(`SELECT * FROM memories WHERE pinned = 1 ORDER BY updatedAt DESC LIMIT ?`),
|
|
470
479
|
search: db.prepare(`
|
|
471
480
|
SELECT m.* FROM memories m
|
|
472
481
|
INNER JOIN memories_fts f ON m.rowid = f.rowid
|
|
@@ -479,6 +488,12 @@ function initDb() {
|
|
|
479
488
|
WHERE memories_fts MATCH ? AND m.agentId = ?
|
|
480
489
|
LIMIT ${MAX_FTS_RESULT_ROWS}
|
|
481
490
|
`),
|
|
491
|
+
searchByAgentOrShared: db.prepare(`
|
|
492
|
+
SELECT m.* FROM memories m
|
|
493
|
+
INNER JOIN memories_fts f ON m.rowid = f.rowid
|
|
494
|
+
WHERE memories_fts MATCH ? AND (m.agentId = ? OR m.sharedWith LIKE ?)
|
|
495
|
+
LIMIT ${MAX_FTS_RESULT_ROWS}
|
|
496
|
+
`),
|
|
482
497
|
// Remove a linked ID from all memories that reference it (cleanup on delete)
|
|
483
498
|
findMemoriesLinkingTo: db.prepare(`SELECT * FROM memories WHERE linkedMemoryIds LIKE ?`),
|
|
484
499
|
updateLinks: db.prepare(`UPDATE memories SET linkedMemoryIds = ?, updatedAt = ? WHERE id = ?`),
|
|
@@ -489,6 +504,7 @@ function initDb() {
|
|
|
489
504
|
LIMIT 1
|
|
490
505
|
`),
|
|
491
506
|
allRowsByUpdated: db.prepare(`SELECT * FROM memories ORDER BY updatedAt DESC`),
|
|
507
|
+
countsByAgent: db.prepare(`SELECT COALESCE(agentId, '_global') AS agentKey, COUNT(*) AS cnt FROM memories GROUP BY agentKey`),
|
|
492
508
|
exactDuplicateBySessionCategory: db.prepare(`
|
|
493
509
|
SELECT * FROM memories
|
|
494
510
|
WHERE sessionId = ? AND category = ? AND title = ? AND content = ?
|
|
@@ -517,6 +533,8 @@ function initDb() {
|
|
|
517
533
|
image,
|
|
518
534
|
imagePath: image?.path || undefined,
|
|
519
535
|
linkedMemoryIds: linkedMemoryIds.length ? linkedMemoryIds : undefined,
|
|
536
|
+
pinned: row.pinned === 1,
|
|
537
|
+
sharedWith: parseJsonSafe<string[]>(row.sharedWith, []).length ? parseJsonSafe<string[]>(row.sharedWith, []) : undefined,
|
|
520
538
|
createdAt: typeof row.createdAt === 'number' ? row.createdAt : Date.now(),
|
|
521
539
|
updatedAt: typeof row.updatedAt === 'number' ? row.updatedAt : Date.now(),
|
|
522
540
|
}
|
|
@@ -562,6 +580,8 @@ function initDb() {
|
|
|
562
580
|
const duplicate = stmts.exactDuplicateBySessionCategory.get(sessionId, category, title, content) as Record<string, unknown> | undefined
|
|
563
581
|
if (duplicate) return rowToEntry(duplicate)
|
|
564
582
|
}
|
|
583
|
+
const pinned = data.pinned ? 1 : 0
|
|
584
|
+
const sharedWith = Array.isArray(data.sharedWith) && data.sharedWith.length ? JSON.stringify(data.sharedWith) : null
|
|
565
585
|
stmts.insert.run(
|
|
566
586
|
id, data.agentId || null, sessionId,
|
|
567
587
|
category, title, content,
|
|
@@ -572,6 +592,8 @@ function initDb() {
|
|
|
572
592
|
image ? JSON.stringify(image) : null,
|
|
573
593
|
image?.path || null,
|
|
574
594
|
linkedMemoryIds.length ? JSON.stringify(linkedMemoryIds) : null,
|
|
595
|
+
pinned,
|
|
596
|
+
sharedWith,
|
|
575
597
|
now, now,
|
|
576
598
|
)
|
|
577
599
|
// Compute embedding in background (fire-and-forget)
|
|
@@ -617,6 +639,8 @@ function initDb() {
|
|
|
617
639
|
const nextLinked = normalizeLinkedMemoryIds(merged.linkedMemoryIds, id)
|
|
618
640
|
const prevLinked = normalizeLinkedMemoryIds(existingEntry.linkedMemoryIds, id)
|
|
619
641
|
const now = Date.now()
|
|
642
|
+
const pinnedVal = merged.pinned ? 1 : 0
|
|
643
|
+
const sharedWithVal = Array.isArray(merged.sharedWith) && merged.sharedWith.length ? JSON.stringify(merged.sharedWith) : null
|
|
620
644
|
stmts.update.run(
|
|
621
645
|
merged.agentId || null, merged.sessionId || null,
|
|
622
646
|
merged.category, merged.title, merged.content,
|
|
@@ -627,6 +651,8 @@ function initDb() {
|
|
|
627
651
|
image ? JSON.stringify(image) : null,
|
|
628
652
|
image?.path || null,
|
|
629
653
|
nextLinked.length ? JSON.stringify(nextLinked) : null,
|
|
654
|
+
pinnedVal,
|
|
655
|
+
sharedWithVal,
|
|
630
656
|
now, id,
|
|
631
657
|
)
|
|
632
658
|
|
|
@@ -755,11 +781,11 @@ function initDb() {
|
|
|
755
781
|
search(query: string, agentId?: string): MemoryEntry[] {
|
|
756
782
|
if (shouldSkipSearchQuery(query)) return []
|
|
757
783
|
const startedAt = Date.now()
|
|
758
|
-
// FTS keyword search
|
|
784
|
+
// FTS keyword search (includes memories shared with this agent)
|
|
759
785
|
const ftsQuery = buildFtsQuery(query)
|
|
760
786
|
const ftsResults: MemoryEntry[] = ftsQuery
|
|
761
787
|
? (agentId
|
|
762
|
-
? stmts.
|
|
788
|
+
? stmts.searchByAgentOrShared.all(ftsQuery, agentId, `%"${agentId}"%`) as any[]
|
|
763
789
|
: stmts.search.all(ftsQuery) as any[]
|
|
764
790
|
).map(rowToEntry)
|
|
765
791
|
: []
|
|
@@ -838,11 +864,26 @@ function initDb() {
|
|
|
838
864
|
list(agentId?: string, limit = 200): MemoryEntry[] {
|
|
839
865
|
const safeLimit = Math.max(1, Math.min(500, Math.trunc(limit)))
|
|
840
866
|
const rows = agentId
|
|
841
|
-
? stmts.
|
|
867
|
+
? stmts.listByAgentOrShared.all(agentId, `%"${agentId}"%`, safeLimit) as any[]
|
|
842
868
|
: stmts.listAll.all(safeLimit) as any[]
|
|
843
869
|
return rows.map(rowToEntry)
|
|
844
870
|
},
|
|
845
871
|
|
|
872
|
+
listPinned(agentId?: string, limit = 20): MemoryEntry[] {
|
|
873
|
+
const safeLimit = Math.max(1, Math.min(100, Math.trunc(limit)))
|
|
874
|
+
const rows = agentId
|
|
875
|
+
? stmts.listPinnedByAgent.all(agentId, safeLimit) as any[]
|
|
876
|
+
: stmts.listPinnedAll.all(safeLimit) as any[]
|
|
877
|
+
return rows.map(rowToEntry)
|
|
878
|
+
},
|
|
879
|
+
|
|
880
|
+
countsByAgent(): Record<string, number> {
|
|
881
|
+
const rows = stmts.countsByAgent.all() as { agentKey: string; cnt: number }[]
|
|
882
|
+
const result: Record<string, number> = {}
|
|
883
|
+
for (const row of rows) result[row.agentKey] = row.cnt
|
|
884
|
+
return result
|
|
885
|
+
},
|
|
886
|
+
|
|
846
887
|
getByAgent(agentId: string, limit = 200): MemoryEntry[] {
|
|
847
888
|
const safeLimit = Math.max(1, Math.min(500, Math.trunc(limit)))
|
|
848
889
|
return (stmts.listByAgent.all(agentId, safeLimit) as any[]).map(rowToEntry)
|
|
@@ -1019,6 +1060,8 @@ export function addKnowledge(params: {
|
|
|
1019
1060
|
title: string
|
|
1020
1061
|
content: string
|
|
1021
1062
|
tags?: string[]
|
|
1063
|
+
scope?: 'global' | 'agent'
|
|
1064
|
+
agentIds?: string[]
|
|
1022
1065
|
createdByAgentId?: string | null
|
|
1023
1066
|
createdBySessionId?: string | null
|
|
1024
1067
|
}): MemoryEntry {
|
|
@@ -1031,6 +1074,8 @@ export function addKnowledge(params: {
|
|
|
1031
1074
|
content: params.content,
|
|
1032
1075
|
metadata: {
|
|
1033
1076
|
tags: params.tags || [],
|
|
1077
|
+
scope: params.scope || 'global',
|
|
1078
|
+
agentIds: params.scope === 'agent' ? (params.agentIds || []) : [],
|
|
1034
1079
|
createdByAgentId: params.createdByAgentId || null,
|
|
1035
1080
|
createdBySessionId: params.createdBySessionId || null,
|
|
1036
1081
|
},
|
|
@@ -77,6 +77,7 @@ export class OpenClawGateway {
|
|
|
77
77
|
private _connected = false
|
|
78
78
|
private reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
|
79
79
|
private reconnectDelay = 800
|
|
80
|
+
private consecutiveFailures = 0
|
|
80
81
|
private shouldReconnect = false
|
|
81
82
|
private wsUrl = ''
|
|
82
83
|
private token: string | undefined
|
|
@@ -104,6 +105,7 @@ export class OpenClawGateway {
|
|
|
104
105
|
this.ws = result.ws
|
|
105
106
|
this._connected = true
|
|
106
107
|
this.reconnectDelay = 800
|
|
108
|
+
this.consecutiveFailures = 0
|
|
107
109
|
console.log('[openclaw-gateway] Connected to gateway')
|
|
108
110
|
|
|
109
111
|
this.ws.on('message', (data) => {
|
|
@@ -149,12 +151,18 @@ export class OpenClawGateway {
|
|
|
149
151
|
|
|
150
152
|
private scheduleReconnect() {
|
|
151
153
|
if (this.reconnectTimer || !this.shouldReconnect) return
|
|
154
|
+
this.consecutiveFailures++
|
|
155
|
+
// After many failures, back off to 10 minutes to avoid hammering a down server
|
|
156
|
+
const maxDelay = this.consecutiveFailures >= 10 ? 600_000 : 15_000
|
|
152
157
|
this.reconnectTimer = setTimeout(() => {
|
|
153
158
|
this.reconnectTimer = null
|
|
154
159
|
if (!this.shouldReconnect) return
|
|
155
160
|
this.doConnect().catch(() => {})
|
|
156
161
|
}, this.reconnectDelay)
|
|
157
|
-
this.reconnectDelay = Math.min(this.reconnectDelay * 2,
|
|
162
|
+
this.reconnectDelay = Math.min(this.reconnectDelay * 2, maxDelay)
|
|
163
|
+
if (this.consecutiveFailures % 5 === 0) {
|
|
164
|
+
console.log(`[openclaw-gateway] ${this.consecutiveFailures} consecutive failures, next retry in ${Math.round(this.reconnectDelay / 1000)}s`)
|
|
165
|
+
}
|
|
158
166
|
}
|
|
159
167
|
|
|
160
168
|
private rejectAllPending(reason: string) {
|
|
@@ -111,3 +111,128 @@ export function getProviderHealthSnapshot(): Record<string, ProviderHealthState
|
|
|
111
111
|
return out
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Lightweight provider ping functions (extracted from check-provider/route.ts)
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
const PING_TIMEOUT_MS = 8_000
|
|
119
|
+
|
|
120
|
+
async function parseErrorMessage(res: Response, fallback: string): Promise<string> {
|
|
121
|
+
const text = await res.text().catch(() => '')
|
|
122
|
+
if (!text) return fallback
|
|
123
|
+
try {
|
|
124
|
+
const parsed = JSON.parse(text)
|
|
125
|
+
if (typeof parsed?.error?.message === 'string' && parsed.error.message.trim()) return parsed.error.message.trim()
|
|
126
|
+
if (typeof parsed?.error === 'string' && parsed.error.trim()) return parsed.error.trim()
|
|
127
|
+
if (typeof parsed?.message === 'string' && parsed.message.trim()) return parsed.message.trim()
|
|
128
|
+
if (typeof parsed?.detail === 'string' && parsed.detail.trim()) return parsed.detail.trim()
|
|
129
|
+
} catch { /* non-JSON */ }
|
|
130
|
+
return text.slice(0, 300).trim() || fallback
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export const OPENAI_COMPATIBLE_DEFAULTS: Record<string, { name: string; defaultEndpoint: string }> = {
|
|
134
|
+
openai: { name: 'OpenAI', defaultEndpoint: 'https://api.openai.com/v1' },
|
|
135
|
+
google: { name: 'Google Gemini', defaultEndpoint: 'https://generativelanguage.googleapis.com/v1beta/openai' },
|
|
136
|
+
deepseek: { name: 'DeepSeek', defaultEndpoint: 'https://api.deepseek.com/v1' },
|
|
137
|
+
groq: { name: 'Groq', defaultEndpoint: 'https://api.groq.com/openai/v1' },
|
|
138
|
+
together: { name: 'Together AI', defaultEndpoint: 'https://api.together.xyz/v1' },
|
|
139
|
+
mistral: { name: 'Mistral AI', defaultEndpoint: 'https://api.mistral.ai/v1' },
|
|
140
|
+
xai: { name: 'xAI (Grok)', defaultEndpoint: 'https://api.x.ai/v1' },
|
|
141
|
+
fireworks: { name: 'Fireworks AI', defaultEndpoint: 'https://api.fireworks.ai/inference/v1' },
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function pingOpenAiCompatible(
|
|
145
|
+
apiKey: string,
|
|
146
|
+
endpoint: string,
|
|
147
|
+
): Promise<{ ok: boolean; message: string }> {
|
|
148
|
+
const normalizedEndpoint = endpoint.replace(/\/+$/, '')
|
|
149
|
+
const res = await fetch(`${normalizedEndpoint}/models`, {
|
|
150
|
+
headers: { authorization: `Bearer ${apiKey}` },
|
|
151
|
+
signal: AbortSignal.timeout(PING_TIMEOUT_MS),
|
|
152
|
+
cache: 'no-store',
|
|
153
|
+
})
|
|
154
|
+
if (!res.ok) {
|
|
155
|
+
const detail = await parseErrorMessage(res, `Provider returned ${res.status}.`)
|
|
156
|
+
return { ok: false, message: detail }
|
|
157
|
+
}
|
|
158
|
+
return { ok: true, message: 'Connected.' }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function pingAnthropic(apiKey: string): Promise<{ ok: boolean; message: string }> {
|
|
162
|
+
const res = await fetch('https://api.anthropic.com/v1/models', {
|
|
163
|
+
headers: {
|
|
164
|
+
'x-api-key': apiKey,
|
|
165
|
+
'anthropic-version': '2023-06-01',
|
|
166
|
+
},
|
|
167
|
+
signal: AbortSignal.timeout(PING_TIMEOUT_MS),
|
|
168
|
+
cache: 'no-store',
|
|
169
|
+
})
|
|
170
|
+
if (!res.ok) {
|
|
171
|
+
const detail = await parseErrorMessage(res, `Anthropic returned ${res.status}.`)
|
|
172
|
+
return { ok: false, message: detail }
|
|
173
|
+
}
|
|
174
|
+
return { ok: true, message: 'Connected to Anthropic.' }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function pingOllama(endpoint: string): Promise<{ ok: boolean; message: string }> {
|
|
178
|
+
const normalizedEndpoint = (endpoint || 'http://localhost:11434').replace(/\/+$/, '')
|
|
179
|
+
const res = await fetch(`${normalizedEndpoint}/api/tags`, {
|
|
180
|
+
signal: AbortSignal.timeout(PING_TIMEOUT_MS),
|
|
181
|
+
cache: 'no-store',
|
|
182
|
+
})
|
|
183
|
+
if (!res.ok) {
|
|
184
|
+
const detail = await parseErrorMessage(res, `Ollama returned ${res.status}.`)
|
|
185
|
+
return { ok: false, message: detail }
|
|
186
|
+
}
|
|
187
|
+
return { ok: true, message: 'Connected to Ollama.' }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function pingOpenClaw(
|
|
191
|
+
apiKey: string | undefined,
|
|
192
|
+
endpoint: string,
|
|
193
|
+
): Promise<{ ok: boolean; message: string }> {
|
|
194
|
+
const { wsConnect } = await import('@/lib/providers/openclaw')
|
|
195
|
+
let url = (endpoint || 'http://localhost:18789').replace(/\/+$/, '')
|
|
196
|
+
if (!/^(https?|wss?):\/\//i.test(url)) url = `http://${url}`
|
|
197
|
+
const wsUrl = url.replace(/^http:/i, 'ws:').replace(/^https:/i, 'wss:')
|
|
198
|
+
const result = await wsConnect(wsUrl, apiKey || undefined, true, PING_TIMEOUT_MS)
|
|
199
|
+
if (result.ws) try { result.ws.close() } catch { /* ignore */ }
|
|
200
|
+
return { ok: result.ok, message: result.ok ? 'Connected to OpenClaw.' : result.message }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Ping a provider to check reachability. Returns `{ ok, message }`.
|
|
205
|
+
* Skips CLI-based providers (claude-cli, codex-cli, opencode-cli) — returns ok.
|
|
206
|
+
*/
|
|
207
|
+
export async function pingProvider(
|
|
208
|
+
provider: string,
|
|
209
|
+
apiKey: string | undefined,
|
|
210
|
+
endpoint: string | undefined,
|
|
211
|
+
): Promise<{ ok: boolean; message: string }> {
|
|
212
|
+
const CLI_PROVIDERS = ['claude-cli', 'codex-cli', 'opencode-cli']
|
|
213
|
+
if (CLI_PROVIDERS.includes(provider)) return { ok: true, message: 'CLI provider — skipped.' }
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
if (provider === 'anthropic') {
|
|
217
|
+
if (!apiKey) return { ok: false, message: 'No API key configured.' }
|
|
218
|
+
return await pingAnthropic(apiKey)
|
|
219
|
+
}
|
|
220
|
+
if (provider === 'ollama') {
|
|
221
|
+
return await pingOllama(endpoint || 'http://localhost:11434')
|
|
222
|
+
}
|
|
223
|
+
if (provider === 'openclaw') {
|
|
224
|
+
return await pingOpenClaw(apiKey, endpoint || 'http://localhost:18789')
|
|
225
|
+
}
|
|
226
|
+
// OpenAI-compatible providers (openai, google, deepseek, groq, together, mistral, xai, fireworks, custom)
|
|
227
|
+
const defaults = OPENAI_COMPATIBLE_DEFAULTS[provider]
|
|
228
|
+
const resolvedEndpoint = endpoint || defaults?.defaultEndpoint
|
|
229
|
+
if (!resolvedEndpoint) return { ok: false, message: `No endpoint for provider "${provider}".` }
|
|
230
|
+
if (!apiKey) return { ok: false, message: 'No API key configured.' }
|
|
231
|
+
return await pingOpenAiCompatible(apiKey, resolvedEndpoint)
|
|
232
|
+
} catch (err: unknown) {
|
|
233
|
+
const msg = err instanceof Error && err.name === 'TimeoutError'
|
|
234
|
+
? 'Connection timed out.'
|
|
235
|
+
: (err instanceof Error ? err.message : String(err))
|
|
236
|
+
return { ok: false, message: msg }
|
|
237
|
+
}
|
|
238
|
+
}
|
package/src/lib/server/queue.ts
CHANGED
|
@@ -12,7 +12,8 @@ import { getCheckpointSaver } from './langgraph-checkpoint'
|
|
|
12
12
|
import { isProtectedMainSession } from './main-session'
|
|
13
13
|
import type { Agent, BoardTask, Message } from '@/types'
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
// HMR-safe: pin processing flag to globalThis so hot reloads don't reset it
|
|
16
|
+
const _queueState = ((globalThis as Record<string, unknown>).__swarmclaw_queue__ ??= { processing: false }) as { processing: boolean }
|
|
16
17
|
|
|
17
18
|
interface SessionMessageLike {
|
|
18
19
|
role?: string
|
|
@@ -467,8 +468,8 @@ function dequeueNextRunnableTask(queue: string[], tasks: Record<string, BoardTas
|
|
|
467
468
|
}
|
|
468
469
|
|
|
469
470
|
export async function processNext() {
|
|
470
|
-
if (processing) return
|
|
471
|
-
processing = true
|
|
471
|
+
if (_queueState.processing) return
|
|
472
|
+
_queueState.processing = true
|
|
472
473
|
|
|
473
474
|
try {
|
|
474
475
|
// Recover orphaned tasks: status is 'queued' but missing from the queue array
|
|
@@ -805,7 +806,7 @@ export async function processNext() {
|
|
|
805
806
|
}
|
|
806
807
|
}
|
|
807
808
|
} finally {
|
|
808
|
-
processing = false
|
|
809
|
+
_queueState.processing = false
|
|
809
810
|
}
|
|
810
811
|
}
|
|
811
812
|
|
|
@@ -4,6 +4,8 @@ import { enqueueTask } from './queue'
|
|
|
4
4
|
import { CronExpressionParser } from 'cron-parser'
|
|
5
5
|
import { pushMainLoopEventToMainSessions } from './main-agent-loop'
|
|
6
6
|
import { getScheduleSignatureKey } from '@/lib/schedule-dedupe'
|
|
7
|
+
import { enqueueSystemEvent } from './system-events'
|
|
8
|
+
import { requestHeartbeatNow } from './heartbeat-wake'
|
|
7
9
|
|
|
8
10
|
const TICK_INTERVAL = 60_000 // 60 seconds
|
|
9
11
|
let intervalId: ReturnType<typeof setInterval> | null = null
|
|
@@ -192,5 +194,11 @@ async function tick() {
|
|
|
192
194
|
type: 'schedule_fired',
|
|
193
195
|
text: `Schedule fired: "${schedule.name}" (${schedule.id}) run #${schedule.runNumber} — task ${taskId}`,
|
|
194
196
|
})
|
|
197
|
+
|
|
198
|
+
// Enqueue system event + heartbeat wake for the schedule's agent
|
|
199
|
+
if (schedule.createdInSessionId) {
|
|
200
|
+
enqueueSystemEvent(schedule.createdInSessionId, `Schedule triggered: ${schedule.name}`)
|
|
201
|
+
}
|
|
202
|
+
requestHeartbeatNow({ agentId: schedule.agentId, reason: 'schedule' })
|
|
195
203
|
}
|
|
196
204
|
}
|
|
@@ -37,6 +37,7 @@ interface QueueEntry {
|
|
|
37
37
|
maxRuntimeMs?: number
|
|
38
38
|
modelOverride?: string
|
|
39
39
|
heartbeatConfig?: { ackMaxChars: number; showOk: boolean; showAlerts: boolean; target: string | null }
|
|
40
|
+
replyToId?: string
|
|
40
41
|
resolve: (value: ExecuteChatTurnResult) => void
|
|
41
42
|
reject: (error: Error) => void
|
|
42
43
|
promise: Promise<ExecuteChatTurnResult>
|
|
@@ -245,6 +246,7 @@ async function drainExecution(executionKey: string): Promise<void> {
|
|
|
245
246
|
onEvent: (event) => emitToSubscribers(next, event),
|
|
246
247
|
modelOverride: next.modelOverride,
|
|
247
248
|
heartbeatConfig: next.heartbeatConfig,
|
|
249
|
+
replyToId: next.replyToId,
|
|
248
250
|
})
|
|
249
251
|
|
|
250
252
|
const failed = !!result.error
|
|
@@ -344,6 +346,7 @@ export interface EnqueueSessionRunInput {
|
|
|
344
346
|
maxRuntimeMs?: number
|
|
345
347
|
modelOverride?: string
|
|
346
348
|
heartbeatConfig?: { ackMaxChars: number; showOk: boolean; showAlerts: boolean; target: string | null }
|
|
349
|
+
replyToId?: string
|
|
347
350
|
}
|
|
348
351
|
|
|
349
352
|
export interface EnqueueSessionRunResult {
|
|
@@ -454,6 +457,7 @@ export function enqueueSessionRun(input: EnqueueSessionRunInput): EnqueueSession
|
|
|
454
457
|
maxRuntimeMs: effectiveMaxRuntimeMs > 0 ? effectiveMaxRuntimeMs : undefined,
|
|
455
458
|
modelOverride: input.modelOverride,
|
|
456
459
|
heartbeatConfig: input.heartbeatConfig,
|
|
460
|
+
replyToId: input.replyToId,
|
|
457
461
|
resolve,
|
|
458
462
|
reject,
|
|
459
463
|
promise,
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
|
+
import { loadChatrooms, saveChatrooms, loadAgents } from '../storage'
|
|
4
|
+
import { genId } from '@/lib/id'
|
|
5
|
+
import { notify } from '../ws-hub'
|
|
6
|
+
import type { ToolBuildContext } from './context'
|
|
7
|
+
import type { Chatroom } from '@/types'
|
|
8
|
+
|
|
9
|
+
export function buildChatroomTools(bctx: ToolBuildContext): StructuredToolInterface[] {
|
|
10
|
+
const tools: StructuredToolInterface[] = []
|
|
11
|
+
const { hasTool } = bctx
|
|
12
|
+
|
|
13
|
+
if (hasTool('manage_chatrooms')) {
|
|
14
|
+
tools.push(
|
|
15
|
+
tool(
|
|
16
|
+
async ({ action, chatroomId, name, description, agentIds, agentId, message }) => {
|
|
17
|
+
try {
|
|
18
|
+
const chatrooms = loadChatrooms() as Record<string, Chatroom>
|
|
19
|
+
|
|
20
|
+
if (action === 'list_chatrooms') {
|
|
21
|
+
const list = Object.values(chatrooms).map((cr) => ({
|
|
22
|
+
id: cr.id,
|
|
23
|
+
name: cr.name,
|
|
24
|
+
description: cr.description,
|
|
25
|
+
memberCount: cr.agentIds.length,
|
|
26
|
+
messageCount: cr.messages.length,
|
|
27
|
+
}))
|
|
28
|
+
return JSON.stringify(list)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (action === 'create_chatroom') {
|
|
32
|
+
const id = genId()
|
|
33
|
+
const agents = loadAgents()
|
|
34
|
+
const validAgentIds = (agentIds || []).filter((aid: string) => agents[aid])
|
|
35
|
+
const chatroom: Chatroom = {
|
|
36
|
+
id,
|
|
37
|
+
name: name || 'New Chatroom',
|
|
38
|
+
description: description || '',
|
|
39
|
+
agentIds: validAgentIds,
|
|
40
|
+
messages: [],
|
|
41
|
+
createdAt: Date.now(),
|
|
42
|
+
updatedAt: Date.now(),
|
|
43
|
+
}
|
|
44
|
+
chatrooms[id] = chatroom
|
|
45
|
+
saveChatrooms(chatrooms)
|
|
46
|
+
notify('chatrooms')
|
|
47
|
+
return JSON.stringify({ ok: true, chatroom: { id, name: chatroom.name, agentIds: validAgentIds } })
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!chatroomId) return 'Error: chatroomId is required for this action.'
|
|
51
|
+
const chatroom = chatrooms[chatroomId]
|
|
52
|
+
if (!chatroom) return `Error: chatroom not found: ${chatroomId}`
|
|
53
|
+
|
|
54
|
+
if (action === 'add_agent') {
|
|
55
|
+
if (!agentId) return 'Error: agentId is required.'
|
|
56
|
+
const agents = loadAgents()
|
|
57
|
+
if (!agents[agentId]) return `Error: agent not found: ${agentId}`
|
|
58
|
+
if (!chatroom.agentIds.includes(agentId)) {
|
|
59
|
+
chatroom.agentIds.push(agentId)
|
|
60
|
+
chatroom.updatedAt = Date.now()
|
|
61
|
+
chatrooms[chatroomId] = chatroom
|
|
62
|
+
saveChatrooms(chatrooms)
|
|
63
|
+
notify('chatrooms')
|
|
64
|
+
notify(`chatroom:${chatroomId}`)
|
|
65
|
+
}
|
|
66
|
+
return JSON.stringify({ ok: true, agentIds: chatroom.agentIds })
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (action === 'remove_agent') {
|
|
70
|
+
if (!agentId) return 'Error: agentId is required.'
|
|
71
|
+
chatroom.agentIds = chatroom.agentIds.filter((id: string) => id !== agentId)
|
|
72
|
+
chatroom.updatedAt = Date.now()
|
|
73
|
+
chatrooms[chatroomId] = chatroom
|
|
74
|
+
saveChatrooms(chatrooms)
|
|
75
|
+
notify('chatrooms')
|
|
76
|
+
notify(`chatroom:${chatroomId}`)
|
|
77
|
+
return JSON.stringify({ ok: true, agentIds: chatroom.agentIds })
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (action === 'list_members') {
|
|
81
|
+
const agents = loadAgents()
|
|
82
|
+
const members = chatroom.agentIds.map((id: string) => {
|
|
83
|
+
const agent = agents[id]
|
|
84
|
+
return agent ? { id, name: agent.name, description: agent.description } : { id, name: 'Unknown' }
|
|
85
|
+
})
|
|
86
|
+
return JSON.stringify(members)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (action === 'send_message') {
|
|
90
|
+
if (!message) return 'Error: message is required.'
|
|
91
|
+
const msgId = genId()
|
|
92
|
+
const senderName = bctx.ctx?.agentId
|
|
93
|
+
? (loadAgents()[bctx.ctx.agentId]?.name || 'Agent')
|
|
94
|
+
: 'Agent'
|
|
95
|
+
chatroom.messages.push({
|
|
96
|
+
id: msgId,
|
|
97
|
+
senderId: bctx.ctx?.agentId || 'agent',
|
|
98
|
+
senderName,
|
|
99
|
+
role: 'assistant' as const,
|
|
100
|
+
text: message,
|
|
101
|
+
mentions: [],
|
|
102
|
+
reactions: [],
|
|
103
|
+
time: Date.now(),
|
|
104
|
+
})
|
|
105
|
+
chatroom.updatedAt = Date.now()
|
|
106
|
+
chatrooms[chatroomId] = chatroom
|
|
107
|
+
saveChatrooms(chatrooms)
|
|
108
|
+
notify(`chatroom:${chatroomId}`)
|
|
109
|
+
return JSON.stringify({ ok: true, messageId: msgId })
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return `Error: unknown action "${action}". Valid actions: list_chatrooms, create_chatroom, add_agent, remove_agent, list_members, send_message`
|
|
113
|
+
} catch (err: unknown) {
|
|
114
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'manage_chatrooms',
|
|
119
|
+
description: 'Manage chatrooms for multi-agent collaboration. Actions: list_chatrooms, create_chatroom, add_agent, remove_agent, list_members, send_message.',
|
|
120
|
+
schema: z.object({
|
|
121
|
+
action: z.enum(['list_chatrooms', 'create_chatroom', 'add_agent', 'remove_agent', 'list_members', 'send_message'])
|
|
122
|
+
.describe('The action to perform'),
|
|
123
|
+
chatroomId: z.string().optional().describe('Chatroom ID (required for most actions except list/create)'),
|
|
124
|
+
name: z.string().optional().describe('Chatroom name (for create_chatroom)'),
|
|
125
|
+
description: z.string().optional().describe('Chatroom description (for create_chatroom)'),
|
|
126
|
+
agentIds: z.array(z.string()).optional().describe('Initial agent IDs (for create_chatroom)'),
|
|
127
|
+
agentId: z.string().optional().describe('Agent ID (for add_agent/remove_agent)'),
|
|
128
|
+
message: z.string().optional().describe('Message text (for send_message)'),
|
|
129
|
+
}),
|
|
130
|
+
},
|
|
131
|
+
),
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return tools
|
|
136
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
|
-
import {
|
|
3
|
+
import { HumanMessage } from '@langchain/core/messages'
|
|
4
|
+
import { loadSessions, saveSessions, loadCredentials, decryptKey } from '../storage'
|
|
5
|
+
import { buildChatModel } from '../build-llm'
|
|
6
|
+
import { getProvider } from '@/lib/providers'
|
|
4
7
|
import type { ToolBuildContext } from './context'
|
|
5
8
|
|
|
6
9
|
export function buildContextTools(bctx: ToolBuildContext): StructuredToolInterface[] {
|
|
@@ -19,8 +22,8 @@ export function buildContextTools(bctx: ToolBuildContext): StructuredToolInterfa
|
|
|
19
22
|
const systemPromptTokens = 2000
|
|
20
23
|
const status = getContextStatus(messages, systemPromptTokens, session.provider, session.model)
|
|
21
24
|
return JSON.stringify(status)
|
|
22
|
-
} catch (err:
|
|
23
|
-
return `Error: ${err.message
|
|
25
|
+
} catch (err: unknown) {
|
|
26
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`
|
|
24
27
|
}
|
|
25
28
|
},
|
|
26
29
|
{
|
|
@@ -46,20 +49,33 @@ export function buildContextTools(bctx: ToolBuildContext): StructuredToolInterfa
|
|
|
46
49
|
return JSON.stringify({ status: 'no_action', reason: 'Not enough messages to compact', messageCount: messages.length })
|
|
47
50
|
}
|
|
48
51
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
52
|
+
// Resolve API key for the session's provider
|
|
53
|
+
let apiKey: string | null = null
|
|
54
|
+
const providerInfo = getProvider(session.provider)
|
|
55
|
+
if ((providerInfo?.requiresApiKey || providerInfo?.optionalApiKey) && session.credentialId) {
|
|
56
|
+
try {
|
|
57
|
+
const creds = loadCredentials()
|
|
58
|
+
const cred = creds[session.credentialId]
|
|
59
|
+
if (cred) apiKey = decryptKey(cred.encryptedKey)
|
|
60
|
+
} catch { /* continue without key */ }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Build LLM summarizer using the session's provider/model
|
|
64
|
+
const generateSummary = async (prompt: string): Promise<string> => {
|
|
65
|
+
const llm = buildChatModel({
|
|
66
|
+
provider: session.provider,
|
|
67
|
+
model: session.model,
|
|
68
|
+
apiKey,
|
|
69
|
+
apiEndpoint: session.apiEndpoint,
|
|
70
|
+
})
|
|
71
|
+
const response = await llm.invoke([new HumanMessage(prompt)])
|
|
72
|
+
if (typeof response.content === 'string') return response.content
|
|
73
|
+
if (Array.isArray(response.content)) {
|
|
74
|
+
return response.content
|
|
75
|
+
.map((b: Record<string, unknown>) => (typeof b.text === 'string' ? b.text : ''))
|
|
76
|
+
.join('')
|
|
61
77
|
}
|
|
62
|
-
return
|
|
78
|
+
return ''
|
|
63
79
|
}
|
|
64
80
|
|
|
65
81
|
const result = await summarizeAndCompact({
|
|
@@ -67,6 +83,8 @@ export function buildContextTools(bctx: ToolBuildContext): StructuredToolInterfa
|
|
|
67
83
|
keepLastN: keep,
|
|
68
84
|
agentId: ctx?.agentId || session.agentId || null,
|
|
69
85
|
sessionId: ctx.sessionId,
|
|
86
|
+
provider: session.provider,
|
|
87
|
+
model: session.model,
|
|
70
88
|
generateSummary,
|
|
71
89
|
})
|
|
72
90
|
|
|
@@ -84,8 +102,8 @@ export function buildContextTools(bctx: ToolBuildContext): StructuredToolInterfa
|
|
|
84
102
|
summaryAdded: result.summaryAdded,
|
|
85
103
|
remainingMessages: result.messages.length,
|
|
86
104
|
})
|
|
87
|
-
} catch (err:
|
|
88
|
-
return `Error: ${err.message
|
|
105
|
+
} catch (err: unknown) {
|
|
106
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`
|
|
89
107
|
}
|
|
90
108
|
},
|
|
91
109
|
{
|
|
@@ -16,6 +16,7 @@ import { buildConnectorTools } from './connector'
|
|
|
16
16
|
import { buildContextTools } from './context-mgmt'
|
|
17
17
|
import { buildSandboxTools } from './sandbox'
|
|
18
18
|
import { buildOpenClawNodeTools } from './openclaw-nodes'
|
|
19
|
+
import { buildChatroomTools } from './chatroom'
|
|
19
20
|
|
|
20
21
|
export type { ToolContext, SessionToolsResult }
|
|
21
22
|
export { sweepOrphanedBrowsers, cleanupSessionBrowser, getActiveBrowserCount, hasActiveBrowser }
|
|
@@ -97,6 +98,7 @@ export async function buildSessionTools(cwd: string, enabledTools: string[], ctx
|
|
|
97
98
|
...buildContextTools(bctx),
|
|
98
99
|
...buildSandboxTools(bctx),
|
|
99
100
|
...buildOpenClawNodeTools(bctx),
|
|
101
|
+
...buildChatroomTools(bctx),
|
|
100
102
|
)
|
|
101
103
|
|
|
102
104
|
// ---------------------------------------------------------------------------
|