@swarmclawai/swarmclaw 0.5.3 → 0.6.2
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 +53 -9
- package/bin/server-cmd.js +1 -0
- package/bin/swarmclaw.js +76 -16
- package/next.config.ts +11 -1
- package/package.json +5 -2
- package/scripts/postinstall.mjs +18 -0
- package/src/app/api/canvas/[sessionId]/route.ts +31 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +284 -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/connectors/[id]/route.ts +1 -0
- package/src/app/api/connectors/route.ts +2 -1
- package/src/app/api/credentials/route.ts +2 -3
- package/src/app/api/files/open/route.ts +43 -0
- 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/route.ts +4 -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 +53 -1
- 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/[id]/messages/route.ts +70 -2
- package/src/app/api/sessions/[id]/route.ts +4 -0
- 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 +12 -0
- package/src/app/api/tasks/bulk/route.ts +100 -0
- package/src/app/api/tasks/metrics/route.ts +101 -0
- package/src/app/api/tasks/route.ts +18 -2
- package/src/app/api/tts/route.ts +3 -2
- package/src/app/api/tts/stream/route.ts +3 -2
- package/src/app/api/uploads/[filename]/route.ts +19 -34
- package/src/app/api/uploads/route.ts +94 -0
- package/src/app/api/webhooks/[id]/route.ts +15 -1
- package/src/app/globals.css +63 -15
- package/src/app/page.tsx +142 -13
- package/src/cli/index.js +40 -1
- package/src/cli/index.test.js +30 -0
- package/src/cli/spec.js +42 -0
- package/src/components/agents/agent-avatar.tsx +57 -10
- package/src/components/agents/agent-card.tsx +50 -17
- package/src/components/agents/agent-chat-list.tsx +148 -12
- package/src/components/agents/agent-list.tsx +50 -19
- package/src/components/agents/agent-sheet.tsx +120 -65
- package/src/components/agents/inspector-panel.tsx +81 -6
- package/src/components/agents/openclaw-skills-panel.tsx +32 -3
- package/src/components/agents/personality-builder.tsx +42 -14
- package/src/components/agents/soul-library-picker.tsx +89 -0
- 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/canvas/canvas-panel.tsx +96 -0
- package/src/components/chat/activity-moment.tsx +173 -0
- package/src/components/chat/chat-area.tsx +46 -22
- package/src/components/chat/chat-header.tsx +457 -286
- package/src/components/chat/chat-preview-panel.tsx +1 -2
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/delegation-banner.tsx +371 -0
- package/src/components/chat/file-path-chip.tsx +146 -0
- package/src/components/chat/heartbeat-history-panel.tsx +269 -0
- package/src/components/chat/markdown-utils.ts +9 -0
- package/src/components/chat/message-bubble.tsx +356 -315
- package/src/components/chat/message-list.tsx +230 -8
- package/src/components/chat/streaming-bubble.tsx +104 -47
- 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 +111 -73
- 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 +130 -0
- package/src/components/chatrooms/chatroom-message.tsx +432 -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-list.tsx +168 -90
- package/src/components/connectors/connector-sheet.tsx +95 -56
- package/src/components/home/home-view.tsx +501 -0
- package/src/components/input/chat-input.tsx +107 -43
- 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 +194 -97
- 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 +1 -1
- package/src/components/schedules/schedule-list.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +259 -126
- 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/settings/gateway-disconnect-overlay.tsx +80 -0
- 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/chatroom-picker-list.tsx +61 -0
- package/src/components/shared/check-icon.tsx +12 -0
- package/src/components/shared/confirm-dialog.tsx +1 -1
- package/src/components/shared/connector-platform-icon.tsx +51 -4
- 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/icon-button.tsx +16 -2
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
- package/src/components/shared/notification-center.tsx +44 -6
- 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 +31 -15
- 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-embedding.tsx +48 -13
- package/src/components/shared/settings/section-orchestrator.tsx +46 -15
- 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-storage.tsx +206 -0
- package/src/components/shared/settings/section-theme.tsx +95 -0
- package/src/components/shared/settings/section-user-preferences.tsx +57 -0
- package/src/components/shared/settings/section-voice.tsx +42 -21
- package/src/components/shared/settings/section-web-search.tsx +30 -6
- package/src/components/shared/settings/settings-page.tsx +182 -27
- package/src/components/shared/settings/settings-sheet.tsx +9 -73
- package/src/components/shared/settings/storage-browser.tsx +259 -0
- 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 +59 -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 +416 -74
- package/src/components/ui/hover-card.tsx +52 -0
- package/src/components/usage/metrics-dashboard.tsx +90 -6
- package/src/components/usage/usage-list.tsx +1 -1
- package/src/components/webhooks/webhook-sheet.tsx +1 -1
- package/src/hooks/use-continuous-speech.ts +10 -4
- package/src/hooks/use-view-router.ts +69 -19
- package/src/hooks/use-voice-conversation.ts +53 -10
- package/src/hooks/use-ws.ts +4 -2
- package/src/instrumentation.ts +15 -1
- package/src/lib/chat.ts +2 -0
- package/src/lib/memory.ts +3 -0
- package/src/lib/providers/anthropic.ts +13 -7
- package/src/lib/providers/index.ts +1 -0
- package/src/lib/providers/openai.ts +13 -7
- package/src/lib/server/chat-execution.ts +75 -15
- package/src/lib/server/chatroom-helpers.ts +146 -0
- package/src/lib/server/connectors/manager.ts +229 -7
- package/src/lib/server/context-manager.ts +225 -13
- package/src/lib/server/create-notification.ts +14 -2
- package/src/lib/server/daemon-state.ts +157 -10
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service.ts +48 -6
- package/src/lib/server/heartbeat-wake.ts +110 -0
- package/src/lib/server/langgraph-checkpoint.ts +1 -0
- package/src/lib/server/main-agent-loop.ts +1 -1
- package/src/lib/server/memory-consolidation.ts +105 -0
- package/src/lib/server/memory-db.ts +183 -10
- package/src/lib/server/mime.ts +51 -0
- package/src/lib/server/openclaw-gateway.ts +9 -1
- package/src/lib/server/orchestrator-lg.ts +2 -0
- package/src/lib/server/orchestrator.ts +5 -2
- package/src/lib/server/playwright-proxy.mjs +2 -3
- package/src/lib/server/prompt-runtime-context.ts +53 -0
- package/src/lib/server/provider-health.ts +125 -0
- package/src/lib/server/queue.ts +56 -10
- package/src/lib/server/scheduler.ts +8 -0
- package/src/lib/server/session-run-manager.ts +4 -0
- package/src/lib/server/session-tools/canvas.ts +67 -0
- package/src/lib/server/session-tools/chatroom.ts +136 -0
- package/src/lib/server/session-tools/connector.ts +83 -9
- package/src/lib/server/session-tools/context-mgmt.ts +36 -18
- package/src/lib/server/session-tools/crud.ts +21 -0
- package/src/lib/server/session-tools/delegate.ts +68 -4
- package/src/lib/server/session-tools/git.ts +71 -0
- package/src/lib/server/session-tools/http.ts +57 -0
- package/src/lib/server/session-tools/index.ts +10 -0
- package/src/lib/server/session-tools/memory.ts +7 -1
- package/src/lib/server/session-tools/search-providers.ts +16 -8
- package/src/lib/server/session-tools/subagent.ts +106 -0
- package/src/lib/server/session-tools/web.ts +115 -4
- package/src/lib/server/storage.ts +53 -29
- package/src/lib/server/stream-agent-chat.ts +185 -57
- package/src/lib/server/system-events.ts +49 -0
- package/src/lib/server/task-mention.ts +41 -0
- package/src/lib/server/ws-hub.ts +11 -0
- package/src/lib/sessions.ts +10 -0
- package/src/lib/soul-library.ts +103 -0
- package/src/lib/soul-suggestions.ts +109 -0
- package/src/lib/task-dedupe.ts +26 -0
- package/src/lib/tasks.ts +4 -1
- package/src/lib/tool-definitions.ts +2 -0
- package/src/lib/tts.ts +2 -2
- package/src/lib/view-routes.ts +36 -1
- package/src/lib/ws-client.ts +14 -4
- package/src/stores/use-app-store.ts +41 -3
- package/src/stores/use-chat-store.ts +113 -5
- package/src/stores/use-chatroom-store.ts +276 -0
- package/src/types/index.ts +88 -4
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Database from 'better-sqlite3'
|
|
2
2
|
import path from 'path'
|
|
3
3
|
import fs from 'fs'
|
|
4
|
+
import { createHash } from 'crypto'
|
|
4
5
|
import { genId } from '@/lib/id'
|
|
5
6
|
import type { MemoryEntry, FileReference, MemoryImage, MemoryReference } from '@/types'
|
|
6
7
|
import { getEmbedding, cosineSimilarity, serializeEmbedding, deserializeEmbedding } from './embeddings'
|
|
@@ -32,6 +33,11 @@ export const MEMORY_FTS_STOP_WORDS = new Set([
|
|
|
32
33
|
'you', 'your',
|
|
33
34
|
])
|
|
34
35
|
|
|
36
|
+
function computeContentHash(category: string, content: string): string {
|
|
37
|
+
const normalized = `${category}|${content.toLowerCase().trim()}`
|
|
38
|
+
return createHash('sha256').update(normalized).digest('hex').slice(0, 16)
|
|
39
|
+
}
|
|
40
|
+
|
|
35
41
|
function shouldSkipSearchQuery(input: string): boolean {
|
|
36
42
|
const text = String(input || '').toLowerCase().trim()
|
|
37
43
|
if (!text) return true
|
|
@@ -329,6 +335,7 @@ function normalizeImage(rawImage: unknown, legacyImagePath?: string | null): Mem
|
|
|
329
335
|
function initDb() {
|
|
330
336
|
const db = new Database(DB_PATH)
|
|
331
337
|
db.pragma('journal_mode = WAL')
|
|
338
|
+
db.pragma('busy_timeout = 5000')
|
|
332
339
|
|
|
333
340
|
db.exec(`
|
|
334
341
|
CREATE TABLE IF NOT EXISTS memories (
|
|
@@ -354,10 +361,22 @@ function initDb() {
|
|
|
354
361
|
'linkedMemoryIds TEXT',
|
|
355
362
|
'"references" TEXT',
|
|
356
363
|
'image TEXT',
|
|
364
|
+
'pinned INTEGER DEFAULT 0',
|
|
365
|
+
'sharedWith TEXT',
|
|
366
|
+
'accessCount INTEGER DEFAULT 0',
|
|
367
|
+
'lastAccessedAt INTEGER DEFAULT 0',
|
|
368
|
+
'contentHash TEXT',
|
|
369
|
+
'reinforcementCount INTEGER DEFAULT 0',
|
|
357
370
|
]) {
|
|
358
371
|
try { db.exec(`ALTER TABLE memories ADD COLUMN ${col}`) } catch { /* already exists */ }
|
|
359
372
|
}
|
|
360
373
|
|
|
374
|
+
// Partial index for fast pinned-memory lookups
|
|
375
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_memories_pinned ON memories(agentId, updatedAt DESC) WHERE pinned = 1`)
|
|
376
|
+
|
|
377
|
+
// Index for content hash dedup lookups
|
|
378
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_memories_content_hash ON memories(contentHash) WHERE contentHash IS NOT NULL`)
|
|
379
|
+
|
|
361
380
|
// FTS5 virtual table for full-text search
|
|
362
381
|
db.exec(`
|
|
363
382
|
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
@@ -441,6 +460,24 @@ function initDb() {
|
|
|
441
460
|
})
|
|
442
461
|
migrateLegacyRows()
|
|
443
462
|
|
|
463
|
+
// Backfill contentHash for existing rows that don't have one yet
|
|
464
|
+
const unhashed = (db.prepare(`SELECT COUNT(*) as cnt FROM memories WHERE contentHash IS NULL`).get() as { cnt: number }).cnt
|
|
465
|
+
if (unhashed > 0) {
|
|
466
|
+
const backfillRows = db.prepare(`SELECT id, category, content FROM memories WHERE contentHash IS NULL`).all() as Array<{ id: string; category: string; content: string }>
|
|
467
|
+
const backfillStmt = db.prepare(`UPDATE memories SET contentHash = ? WHERE id = ?`)
|
|
468
|
+
const BATCH = 500
|
|
469
|
+
for (let i = 0; i < backfillRows.length; i += BATCH) {
|
|
470
|
+
const batch = backfillRows.slice(i, i + BATCH)
|
|
471
|
+
const tx = db.transaction(() => {
|
|
472
|
+
for (const r of batch) {
|
|
473
|
+
backfillStmt.run(computeContentHash(r.category, r.content), r.id)
|
|
474
|
+
}
|
|
475
|
+
})
|
|
476
|
+
tx()
|
|
477
|
+
}
|
|
478
|
+
console.log(`[memory-db] Backfilled contentHash for ${backfillRows.length} memory row(s)`)
|
|
479
|
+
}
|
|
480
|
+
|
|
444
481
|
// Fresh installs now start with an empty memory graph.
|
|
445
482
|
// Durable memories are created only from actual user/agent interactions.
|
|
446
483
|
|
|
@@ -448,14 +485,14 @@ function initDb() {
|
|
|
448
485
|
insert: db.prepare(`
|
|
449
486
|
INSERT INTO memories (
|
|
450
487
|
id, agentId, sessionId, category, title, content, metadata, embedding,
|
|
451
|
-
"references", filePaths, image, imagePath, linkedMemoryIds, createdAt, updatedAt
|
|
488
|
+
"references", filePaths, image, imagePath, linkedMemoryIds, pinned, sharedWith, contentHash, createdAt, updatedAt
|
|
452
489
|
)
|
|
453
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
490
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
454
491
|
`),
|
|
455
492
|
update: db.prepare(`
|
|
456
493
|
UPDATE memories
|
|
457
494
|
SET agentId=?, sessionId=?, category=?, title=?, content=?, metadata=?, embedding=?,
|
|
458
|
-
"references"=?, filePaths=?, image=?, imagePath=?, linkedMemoryIds=?, updatedAt=?
|
|
495
|
+
"references"=?, filePaths=?, image=?, imagePath=?, linkedMemoryIds=?, pinned=?, sharedWith=?, updatedAt=?
|
|
459
496
|
WHERE id=?
|
|
460
497
|
`),
|
|
461
498
|
delete: db.prepare(`DELETE FROM memories WHERE id=?`),
|
|
@@ -467,6 +504,9 @@ function initDb() {
|
|
|
467
504
|
},
|
|
468
505
|
listAll: db.prepare(`SELECT * FROM memories ORDER BY updatedAt DESC LIMIT ?`),
|
|
469
506
|
listByAgent: db.prepare(`SELECT * FROM memories WHERE agentId=? ORDER BY updatedAt DESC LIMIT ?`),
|
|
507
|
+
listByAgentOrShared: db.prepare(`SELECT * FROM memories WHERE agentId=? OR sharedWith LIKE ? ORDER BY updatedAt DESC LIMIT ?`),
|
|
508
|
+
listPinnedByAgent: db.prepare(`SELECT * FROM memories WHERE pinned = 1 AND agentId = ? ORDER BY updatedAt DESC LIMIT ?`),
|
|
509
|
+
listPinnedAll: db.prepare(`SELECT * FROM memories WHERE pinned = 1 ORDER BY updatedAt DESC LIMIT ?`),
|
|
470
510
|
search: db.prepare(`
|
|
471
511
|
SELECT m.* FROM memories m
|
|
472
512
|
INNER JOIN memories_fts f ON m.rowid = f.rowid
|
|
@@ -479,6 +519,12 @@ function initDb() {
|
|
|
479
519
|
WHERE memories_fts MATCH ? AND m.agentId = ?
|
|
480
520
|
LIMIT ${MAX_FTS_RESULT_ROWS}
|
|
481
521
|
`),
|
|
522
|
+
searchByAgentOrShared: db.prepare(`
|
|
523
|
+
SELECT m.* FROM memories m
|
|
524
|
+
INNER JOIN memories_fts f ON m.rowid = f.rowid
|
|
525
|
+
WHERE memories_fts MATCH ? AND (m.agentId = ? OR m.sharedWith LIKE ?)
|
|
526
|
+
LIMIT ${MAX_FTS_RESULT_ROWS}
|
|
527
|
+
`),
|
|
482
528
|
// Remove a linked ID from all memories that reference it (cleanup on delete)
|
|
483
529
|
findMemoriesLinkingTo: db.prepare(`SELECT * FROM memories WHERE linkedMemoryIds LIKE ?`),
|
|
484
530
|
updateLinks: db.prepare(`UPDATE memories SET linkedMemoryIds = ?, updatedAt = ? WHERE id = ?`),
|
|
@@ -489,12 +535,31 @@ function initDb() {
|
|
|
489
535
|
LIMIT 1
|
|
490
536
|
`),
|
|
491
537
|
allRowsByUpdated: db.prepare(`SELECT * FROM memories ORDER BY updatedAt DESC`),
|
|
538
|
+
countsByAgent: db.prepare(`SELECT COALESCE(agentId, '_global') AS agentKey, COUNT(*) AS cnt FROM memories GROUP BY agentKey`),
|
|
492
539
|
exactDuplicateBySessionCategory: db.prepare(`
|
|
493
540
|
SELECT * FROM memories
|
|
494
541
|
WHERE sessionId = ? AND category = ? AND title = ? AND content = ?
|
|
495
542
|
ORDER BY updatedAt DESC
|
|
496
543
|
LIMIT 1
|
|
497
544
|
`),
|
|
545
|
+
findByContentHash: db.prepare(`
|
|
546
|
+
SELECT * FROM memories
|
|
547
|
+
WHERE contentHash = ? AND agentId = ?
|
|
548
|
+
ORDER BY updatedAt DESC
|
|
549
|
+
LIMIT 1
|
|
550
|
+
`),
|
|
551
|
+
findByContentHashShared: db.prepare(`
|
|
552
|
+
SELECT * FROM memories
|
|
553
|
+
WHERE contentHash = ? AND agentId IS NULL
|
|
554
|
+
ORDER BY updatedAt DESC
|
|
555
|
+
LIMIT 1
|
|
556
|
+
`),
|
|
557
|
+
reinforceMemory: db.prepare(`
|
|
558
|
+
UPDATE memories SET reinforcementCount = reinforcementCount + 1, updatedAt = ? WHERE id = ?
|
|
559
|
+
`),
|
|
560
|
+
bumpAccessCount: db.prepare(`
|
|
561
|
+
UPDATE memories SET accessCount = accessCount + 1, lastAccessedAt = ? WHERE id = ?
|
|
562
|
+
`),
|
|
498
563
|
}
|
|
499
564
|
|
|
500
565
|
function rowToEntry(row: Record<string, unknown>): MemoryEntry {
|
|
@@ -517,6 +582,12 @@ function initDb() {
|
|
|
517
582
|
image,
|
|
518
583
|
imagePath: image?.path || undefined,
|
|
519
584
|
linkedMemoryIds: linkedMemoryIds.length ? linkedMemoryIds : undefined,
|
|
585
|
+
pinned: row.pinned === 1,
|
|
586
|
+
sharedWith: parseJsonSafe<string[]>(row.sharedWith, []).length ? parseJsonSafe<string[]>(row.sharedWith, []) : undefined,
|
|
587
|
+
accessCount: typeof row.accessCount === 'number' ? row.accessCount : 0,
|
|
588
|
+
lastAccessedAt: typeof row.lastAccessedAt === 'number' ? row.lastAccessedAt : 0,
|
|
589
|
+
contentHash: typeof row.contentHash === 'string' ? row.contentHash : undefined,
|
|
590
|
+
reinforcementCount: typeof row.reinforcementCount === 'number' ? row.reinforcementCount : 0,
|
|
520
591
|
createdAt: typeof row.createdAt === 'number' ? row.createdAt : Date.now(),
|
|
521
592
|
updatedAt: typeof row.updatedAt === 'number' ? row.updatedAt : Date.now(),
|
|
522
593
|
}
|
|
@@ -556,14 +627,27 @@ function initDb() {
|
|
|
556
627
|
const category = data.category || 'note'
|
|
557
628
|
const title = data.title || 'Untitled'
|
|
558
629
|
const content = data.content || ''
|
|
630
|
+
const contentHash = computeContentHash(category, content)
|
|
631
|
+
|
|
632
|
+
// Content-hash dedup: if same content already exists for this agent, reinforce instead of duplicating
|
|
633
|
+
const agentId = data.agentId || null
|
|
634
|
+
const existingByHash = agentId
|
|
635
|
+
? stmts.findByContentHash.get(contentHash, agentId) as Record<string, unknown> | undefined
|
|
636
|
+
: stmts.findByContentHashShared.get(contentHash) as Record<string, unknown> | undefined
|
|
637
|
+
if (existingByHash) {
|
|
638
|
+
stmts.reinforceMemory.run(now, existingByHash.id)
|
|
639
|
+
return rowToEntry({ ...existingByHash, reinforcementCount: ((existingByHash.reinforcementCount as number) || 0) + 1, updatedAt: now })
|
|
640
|
+
}
|
|
559
641
|
|
|
560
642
|
// Guard against exact duplicate memory spam for the same session/category.
|
|
561
643
|
if (sessionId) {
|
|
562
644
|
const duplicate = stmts.exactDuplicateBySessionCategory.get(sessionId, category, title, content) as Record<string, unknown> | undefined
|
|
563
645
|
if (duplicate) return rowToEntry(duplicate)
|
|
564
646
|
}
|
|
647
|
+
const pinned = data.pinned ? 1 : 0
|
|
648
|
+
const sharedWith = Array.isArray(data.sharedWith) && data.sharedWith.length ? JSON.stringify(data.sharedWith) : null
|
|
565
649
|
stmts.insert.run(
|
|
566
|
-
id,
|
|
650
|
+
id, agentId, sessionId,
|
|
567
651
|
category, title, content,
|
|
568
652
|
data.metadata ? JSON.stringify(data.metadata) : null,
|
|
569
653
|
null, // embedding computed async
|
|
@@ -572,6 +656,9 @@ function initDb() {
|
|
|
572
656
|
image ? JSON.stringify(image) : null,
|
|
573
657
|
image?.path || null,
|
|
574
658
|
linkedMemoryIds.length ? JSON.stringify(linkedMemoryIds) : null,
|
|
659
|
+
pinned,
|
|
660
|
+
sharedWith,
|
|
661
|
+
contentHash,
|
|
575
662
|
now, now,
|
|
576
663
|
)
|
|
577
664
|
// Compute embedding in background (fire-and-forget)
|
|
@@ -601,6 +688,10 @@ function initDb() {
|
|
|
601
688
|
image,
|
|
602
689
|
imagePath: image?.path || null,
|
|
603
690
|
linkedMemoryIds,
|
|
691
|
+
accessCount: 0,
|
|
692
|
+
lastAccessedAt: 0,
|
|
693
|
+
contentHash,
|
|
694
|
+
reinforcementCount: 0,
|
|
604
695
|
createdAt: now,
|
|
605
696
|
updatedAt: now,
|
|
606
697
|
}
|
|
@@ -617,6 +708,8 @@ function initDb() {
|
|
|
617
708
|
const nextLinked = normalizeLinkedMemoryIds(merged.linkedMemoryIds, id)
|
|
618
709
|
const prevLinked = normalizeLinkedMemoryIds(existingEntry.linkedMemoryIds, id)
|
|
619
710
|
const now = Date.now()
|
|
711
|
+
const pinnedVal = merged.pinned ? 1 : 0
|
|
712
|
+
const sharedWithVal = Array.isArray(merged.sharedWith) && merged.sharedWith.length ? JSON.stringify(merged.sharedWith) : null
|
|
620
713
|
stmts.update.run(
|
|
621
714
|
merged.agentId || null, merged.sessionId || null,
|
|
622
715
|
merged.category, merged.title, merged.content,
|
|
@@ -627,6 +720,8 @@ function initDb() {
|
|
|
627
720
|
image ? JSON.stringify(image) : null,
|
|
628
721
|
image?.path || null,
|
|
629
722
|
nextLinked.length ? JSON.stringify(nextLinked) : null,
|
|
723
|
+
pinnedVal,
|
|
724
|
+
sharedWithVal,
|
|
630
725
|
now, id,
|
|
631
726
|
)
|
|
632
727
|
|
|
@@ -673,6 +768,10 @@ function initDb() {
|
|
|
673
768
|
get(id: string): MemoryEntry | null {
|
|
674
769
|
const row = stmts.getById.get(id) as Record<string, unknown> | undefined
|
|
675
770
|
if (!row) return null
|
|
771
|
+
// Bump access count (non-blocking)
|
|
772
|
+
setTimeout(() => {
|
|
773
|
+
try { stmts.bumpAccessCount.run(Date.now(), id) } catch { /* best-effort */ }
|
|
774
|
+
}, 0)
|
|
676
775
|
return rowToEntry(row)
|
|
677
776
|
},
|
|
678
777
|
|
|
@@ -755,16 +854,17 @@ function initDb() {
|
|
|
755
854
|
search(query: string, agentId?: string): MemoryEntry[] {
|
|
756
855
|
if (shouldSkipSearchQuery(query)) return []
|
|
757
856
|
const startedAt = Date.now()
|
|
758
|
-
// FTS keyword search
|
|
857
|
+
// FTS keyword search (includes memories shared with this agent)
|
|
759
858
|
const ftsQuery = buildFtsQuery(query)
|
|
760
859
|
const ftsResults: MemoryEntry[] = ftsQuery
|
|
761
860
|
? (agentId
|
|
762
|
-
? stmts.
|
|
861
|
+
? stmts.searchByAgentOrShared.all(ftsQuery, agentId, `%"${agentId}"%`) as any[]
|
|
763
862
|
: stmts.search.all(ftsQuery) as any[]
|
|
764
863
|
).map(rowToEntry)
|
|
765
864
|
: []
|
|
766
865
|
|
|
767
866
|
// Attempt vector search (synchronous — uses cached embedding if available)
|
|
867
|
+
const vectorSimilarityScores = new Map<string, number>()
|
|
768
868
|
let vectorResults: MemoryEntry[] = []
|
|
769
869
|
try {
|
|
770
870
|
const queryEmbedding = getEmbeddingSync(query)
|
|
@@ -783,13 +883,17 @@ function initDb() {
|
|
|
783
883
|
.sort((a, b) => b.score - a.score)
|
|
784
884
|
.slice(0, 20)
|
|
785
885
|
|
|
786
|
-
vectorResults = scored.map((s) =>
|
|
886
|
+
vectorResults = scored.map((s) => {
|
|
887
|
+
const entry = rowToEntry(s.row)
|
|
888
|
+
vectorSimilarityScores.set(entry.id, s.score)
|
|
889
|
+
return entry
|
|
890
|
+
})
|
|
787
891
|
}
|
|
788
892
|
} catch {
|
|
789
893
|
// Vector search unavailable, use FTS only
|
|
790
894
|
}
|
|
791
895
|
|
|
792
|
-
// Merge: deduplicate by id
|
|
896
|
+
// Merge: deduplicate by id
|
|
793
897
|
const seen = new Set<string>()
|
|
794
898
|
const merged: MemoryEntry[] = []
|
|
795
899
|
for (const entry of [...ftsResults, ...vectorResults]) {
|
|
@@ -798,7 +902,34 @@ function initDb() {
|
|
|
798
902
|
merged.push(entry)
|
|
799
903
|
}
|
|
800
904
|
}
|
|
801
|
-
|
|
905
|
+
|
|
906
|
+
// Apply salience scoring: similarity * recencyDecay * reinforcement * pinnedBoost
|
|
907
|
+
const now = Date.now()
|
|
908
|
+
const HALF_LIFE_DAYS = 30
|
|
909
|
+
const salienceScored = merged.map((entry) => {
|
|
910
|
+
const similarity = vectorSimilarityScores.get(entry.id) ?? 0.5
|
|
911
|
+
const daysSinceAccess = (now - (entry.lastAccessedAt || entry.updatedAt)) / 86_400_000
|
|
912
|
+
const recencyDecay = Math.exp(-0.693 * daysSinceAccess / HALF_LIFE_DAYS)
|
|
913
|
+
const reinforcement = Math.log((entry.reinforcementCount || 0) + 1) + 1
|
|
914
|
+
const pinnedBoost = entry.pinned ? 1.5 : 1.0
|
|
915
|
+
const salience = similarity * recencyDecay * reinforcement * pinnedBoost
|
|
916
|
+
return { entry, salience }
|
|
917
|
+
})
|
|
918
|
+
salienceScored.sort((a, b) => b.salience - a.salience)
|
|
919
|
+
|
|
920
|
+
const out = salienceScored.slice(0, MAX_MERGED_RESULTS).map((s) => s.entry)
|
|
921
|
+
|
|
922
|
+
// Bump access counts for returned results (non-blocking)
|
|
923
|
+
if (out.length) {
|
|
924
|
+
const returnedIds = out.map((e) => e.id)
|
|
925
|
+
setTimeout(() => {
|
|
926
|
+
try {
|
|
927
|
+
const ts = Date.now()
|
|
928
|
+
for (const mid of returnedIds) stmts.bumpAccessCount.run(ts, mid)
|
|
929
|
+
} catch { /* best-effort */ }
|
|
930
|
+
}, 0)
|
|
931
|
+
}
|
|
932
|
+
|
|
802
933
|
const elapsed = Date.now() - startedAt
|
|
803
934
|
if (elapsed > 1200) {
|
|
804
935
|
console.warn(
|
|
@@ -838,11 +969,26 @@ function initDb() {
|
|
|
838
969
|
list(agentId?: string, limit = 200): MemoryEntry[] {
|
|
839
970
|
const safeLimit = Math.max(1, Math.min(500, Math.trunc(limit)))
|
|
840
971
|
const rows = agentId
|
|
841
|
-
? stmts.
|
|
972
|
+
? stmts.listByAgentOrShared.all(agentId, `%"${agentId}"%`, safeLimit) as any[]
|
|
842
973
|
: stmts.listAll.all(safeLimit) as any[]
|
|
843
974
|
return rows.map(rowToEntry)
|
|
844
975
|
},
|
|
845
976
|
|
|
977
|
+
listPinned(agentId?: string, limit = 20): MemoryEntry[] {
|
|
978
|
+
const safeLimit = Math.max(1, Math.min(100, Math.trunc(limit)))
|
|
979
|
+
const rows = agentId
|
|
980
|
+
? stmts.listPinnedByAgent.all(agentId, safeLimit) as any[]
|
|
981
|
+
: stmts.listPinnedAll.all(safeLimit) as any[]
|
|
982
|
+
return rows.map(rowToEntry)
|
|
983
|
+
},
|
|
984
|
+
|
|
985
|
+
countsByAgent(): Record<string, number> {
|
|
986
|
+
const rows = stmts.countsByAgent.all() as { agentKey: string; cnt: number }[]
|
|
987
|
+
const result: Record<string, number> = {}
|
|
988
|
+
for (const row of rows) result[row.agentKey] = row.cnt
|
|
989
|
+
return result
|
|
990
|
+
},
|
|
991
|
+
|
|
846
992
|
getByAgent(agentId: string, limit = 200): MemoryEntry[] {
|
|
847
993
|
const safeLimit = Math.max(1, Math.min(500, Math.trunc(limit)))
|
|
848
994
|
return (stmts.listByAgent.all(agentId, safeLimit) as any[]).map(rowToEntry)
|
|
@@ -924,9 +1070,32 @@ function initDb() {
|
|
|
924
1070
|
const pruneWorking = options.pruneWorking !== false
|
|
925
1071
|
const cutoff = Date.now() - Math.max(1, Math.min(24 * 365, Math.trunc(options.ttlHours || 24))) * 3600_000
|
|
926
1072
|
|
|
1073
|
+
// Hash-based dedup: group by contentHash + agentId, keep the one with highest reinforcementCount
|
|
1074
|
+
if (dedupe && toDelete.size < deleteBudget) {
|
|
1075
|
+
const hashGroups = new Map<string, MemoryEntry[]>()
|
|
1076
|
+
for (const row of rows) {
|
|
1077
|
+
if (!row.contentHash || toDelete.has(row.id)) continue
|
|
1078
|
+
const groupKey = `${row.agentId || ''}|${row.contentHash}`
|
|
1079
|
+
const group = hashGroups.get(groupKey)
|
|
1080
|
+
if (group) group.push(row)
|
|
1081
|
+
else hashGroups.set(groupKey, [row])
|
|
1082
|
+
}
|
|
1083
|
+
for (const group of hashGroups.values()) {
|
|
1084
|
+
if (group.length <= 1) continue
|
|
1085
|
+
group.sort((a, b) => (b.reinforcementCount || 0) - (a.reinforcementCount || 0))
|
|
1086
|
+
for (let i = 1; i < group.length; i++) {
|
|
1087
|
+
toDelete.add(group[i].id)
|
|
1088
|
+
if (toDelete.size >= deleteBudget) break
|
|
1089
|
+
}
|
|
1090
|
+
if (toDelete.size >= deleteBudget) break
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// Exact string-match dedup (legacy fallback for rows without contentHash)
|
|
927
1095
|
if (dedupe) {
|
|
928
1096
|
const seen = new Set<string>()
|
|
929
1097
|
for (const row of rows) {
|
|
1098
|
+
if (toDelete.has(row.id)) continue
|
|
930
1099
|
const key = [
|
|
931
1100
|
row.agentId || '',
|
|
932
1101
|
row.sessionId || '',
|
|
@@ -1019,6 +1188,8 @@ export function addKnowledge(params: {
|
|
|
1019
1188
|
title: string
|
|
1020
1189
|
content: string
|
|
1021
1190
|
tags?: string[]
|
|
1191
|
+
scope?: 'global' | 'agent'
|
|
1192
|
+
agentIds?: string[]
|
|
1022
1193
|
createdByAgentId?: string | null
|
|
1023
1194
|
createdBySessionId?: string | null
|
|
1024
1195
|
}): MemoryEntry {
|
|
@@ -1031,6 +1202,8 @@ export function addKnowledge(params: {
|
|
|
1031
1202
|
content: params.content,
|
|
1032
1203
|
metadata: {
|
|
1033
1204
|
tags: params.tags || [],
|
|
1205
|
+
scope: params.scope || 'global',
|
|
1206
|
+
agentIds: params.scope === 'agent' ? (params.agentIds || []) : [],
|
|
1034
1207
|
createdByAgentId: params.createdByAgentId || null,
|
|
1035
1208
|
createdBySessionId: params.createdBySessionId || null,
|
|
1036
1209
|
},
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export const MIME_TYPES: Record<string, string> = {
|
|
2
|
+
'.png': 'image/png',
|
|
3
|
+
'.jpg': 'image/jpeg',
|
|
4
|
+
'.jpeg': 'image/jpeg',
|
|
5
|
+
'.gif': 'image/gif',
|
|
6
|
+
'.webp': 'image/webp',
|
|
7
|
+
'.svg': 'image/svg+xml',
|
|
8
|
+
'.bmp': 'image/bmp',
|
|
9
|
+
'.ico': 'image/x-icon',
|
|
10
|
+
'.mp4': 'video/mp4',
|
|
11
|
+
'.webm': 'video/webm',
|
|
12
|
+
'.mov': 'video/quicktime',
|
|
13
|
+
'.avi': 'video/x-msvideo',
|
|
14
|
+
'.mkv': 'video/x-matroska',
|
|
15
|
+
'.pdf': 'application/pdf',
|
|
16
|
+
'.json': 'application/json',
|
|
17
|
+
'.csv': 'text/csv',
|
|
18
|
+
'.txt': 'text/plain',
|
|
19
|
+
'.html': 'text/html',
|
|
20
|
+
'.xml': 'application/xml',
|
|
21
|
+
'.zip': 'application/zip',
|
|
22
|
+
'.tar': 'application/x-tar',
|
|
23
|
+
'.gz': 'application/gzip',
|
|
24
|
+
'.doc': 'application/msword',
|
|
25
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
26
|
+
'.xls': 'application/vnd.ms-excel',
|
|
27
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
28
|
+
'.ppt': 'application/vnd.ms-powerpoint',
|
|
29
|
+
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
30
|
+
'.mp3': 'audio/mpeg',
|
|
31
|
+
'.wav': 'audio/wav',
|
|
32
|
+
'.ogg': 'audio/ogg',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.bmp', '.ico'])
|
|
36
|
+
const VIDEO_EXTS = new Set(['.mp4', '.webm', '.mov', '.avi', '.mkv'])
|
|
37
|
+
const AUDIO_EXTS = new Set(['.mp3', '.wav', '.ogg'])
|
|
38
|
+
const DOCUMENT_EXTS = new Set(['.pdf', '.json', '.csv', '.txt', '.html', '.xml', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx'])
|
|
39
|
+
const ARCHIVE_EXTS = new Set(['.zip', '.tar', '.gz'])
|
|
40
|
+
|
|
41
|
+
export type FileCategory = 'image' | 'video' | 'audio' | 'document' | 'archive' | 'other'
|
|
42
|
+
|
|
43
|
+
export function getFileCategory(ext: string): FileCategory {
|
|
44
|
+
const lower = ext.toLowerCase()
|
|
45
|
+
if (IMAGE_EXTS.has(lower)) return 'image'
|
|
46
|
+
if (VIDEO_EXTS.has(lower)) return 'video'
|
|
47
|
+
if (AUDIO_EXTS.has(lower)) return 'audio'
|
|
48
|
+
if (DOCUMENT_EXTS.has(lower)) return 'document'
|
|
49
|
+
if (ARCHIVE_EXTS.has(lower)) return 'archive'
|
|
50
|
+
return 'other'
|
|
51
|
+
}
|
|
@@ -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 === 1 || this.consecutiveFailures % 5 === 0) {
|
|
164
|
+
console.log(`[openclaw-gateway] ${this.consecutiveFailures} consecutive failure${this.consecutiveFailures > 1 ? 's' : ''}, next retry in ${Math.round(this.reconnectDelay / 1000)}s`)
|
|
165
|
+
}
|
|
158
166
|
}
|
|
159
167
|
|
|
160
168
|
private rejectAllPending(reason: string) {
|
|
@@ -11,6 +11,7 @@ import { buildChatModel } from './build-llm'
|
|
|
11
11
|
import { getCheckpointSaver } from './langgraph-checkpoint'
|
|
12
12
|
import { notify } from './ws-hub'
|
|
13
13
|
import { pushMainLoopEventToMainSessions } from './main-agent-loop'
|
|
14
|
+
import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
|
|
14
15
|
import { genId } from '@/lib/id'
|
|
15
16
|
import { NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
|
|
16
17
|
import type { Agent, TaskComment, MessageToolEvent } from '@/types'
|
|
@@ -351,6 +352,7 @@ export async function executeLangGraphOrchestrator(
|
|
|
351
352
|
const settings = loadSettings()
|
|
352
353
|
const promptParts: string[] = []
|
|
353
354
|
if (settings.userPrompt) promptParts.push(settings.userPrompt)
|
|
355
|
+
promptParts.push(buildCurrentDateTimePromptContext())
|
|
354
356
|
if (orchestrator.soul) promptParts.push(orchestrator.soul)
|
|
355
357
|
if (orchestrator.systemPrompt) promptParts.push(orchestrator.systemPrompt)
|
|
356
358
|
// Inject dynamic skills
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
import { WORKSPACE_DIR } from './data-dir'
|
|
7
7
|
import { loadRuntimeSettings, getLegacyOrchestratorMaxTurns } from './runtime-settings'
|
|
8
8
|
import { getMemoryDb } from './memory-db'
|
|
9
|
+
import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
|
|
9
10
|
import { getProvider } from '../providers'
|
|
10
11
|
import type { Agent } from '@/types'
|
|
11
12
|
|
|
@@ -109,6 +110,7 @@ async function executeOrchestratorLegacy(
|
|
|
109
110
|
const settings = loadSettings()
|
|
110
111
|
const promptParts: string[] = []
|
|
111
112
|
if (settings.userPrompt) promptParts.push(settings.userPrompt)
|
|
113
|
+
promptParts.push(buildCurrentDateTimePromptContext())
|
|
112
114
|
if (orchestrator.soul) promptParts.push(orchestrator.soul)
|
|
113
115
|
if (orchestrator.systemPrompt) promptParts.push(orchestrator.systemPrompt)
|
|
114
116
|
if (orchestrator.skillIds?.length) {
|
|
@@ -308,8 +310,8 @@ async function executeSubTask(
|
|
|
308
310
|
|
|
309
311
|
export async function callProvider(
|
|
310
312
|
agent: Agent,
|
|
311
|
-
systemPrompt
|
|
312
|
-
history: { role: string; text: string }[],
|
|
313
|
+
systemPrompt?: string,
|
|
314
|
+
history: { role: string; text: string }[] = [],
|
|
313
315
|
): Promise<string> {
|
|
314
316
|
const provider = getProvider(agent.provider)
|
|
315
317
|
if (!provider) throw new Error(`Unknown provider: ${agent.provider}`)
|
|
@@ -346,6 +348,7 @@ export async function callProvider(
|
|
|
346
348
|
session: mockSession,
|
|
347
349
|
message: history[history.length - 1].text,
|
|
348
350
|
apiKey,
|
|
351
|
+
systemPrompt,
|
|
349
352
|
write: (data: string) => {
|
|
350
353
|
// Parse SSE data to extract text
|
|
351
354
|
if (data.startsWith('data: ')) {
|
|
@@ -7,9 +7,8 @@
|
|
|
7
7
|
import { spawn } from 'child_process'
|
|
8
8
|
import fs from 'fs'
|
|
9
9
|
import path from 'path'
|
|
10
|
-
import os from 'os'
|
|
11
10
|
|
|
12
|
-
const UPLOAD_DIR = process.env.SWARMCLAW_UPLOAD_DIR || path.join(
|
|
11
|
+
const UPLOAD_DIR = process.env.SWARMCLAW_UPLOAD_DIR || path.join(process.env.DATA_DIR || path.join(process.cwd(), 'data'), 'uploads')
|
|
13
12
|
if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true })
|
|
14
13
|
|
|
15
14
|
const child = spawn('npx', ['@playwright/mcp@latest'], {
|
|
@@ -47,7 +46,7 @@ child.stdout.on('data', (chunk) => {
|
|
|
47
46
|
fs.writeFileSync(path.join(UPLOAD_DIR, filename), Buffer.from(block.data, 'base64'))
|
|
48
47
|
newContent.push({
|
|
49
48
|
type: 'text',
|
|
50
|
-
text: `Screenshot saved
|
|
49
|
+
text: `Screenshot saved to /api/uploads/${filename} — it is already displayed inline above (do not repeat it with markdown).`,
|
|
51
50
|
})
|
|
52
51
|
newContent.push(block) // keep image so Claude can see it
|
|
53
52
|
} else {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
function resolveLocalTimezone(): string {
|
|
2
|
+
try {
|
|
3
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
|
|
4
|
+
} catch {
|
|
5
|
+
return 'UTC'
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function formatDateTimeInTimezone(date: Date, timezone: string): string | null {
|
|
10
|
+
try {
|
|
11
|
+
return new Intl.DateTimeFormat('en-US', {
|
|
12
|
+
weekday: 'long',
|
|
13
|
+
year: 'numeric',
|
|
14
|
+
month: 'long',
|
|
15
|
+
day: 'numeric',
|
|
16
|
+
hour: '2-digit',
|
|
17
|
+
minute: '2-digit',
|
|
18
|
+
second: '2-digit',
|
|
19
|
+
hour12: false,
|
|
20
|
+
timeZone: timezone,
|
|
21
|
+
timeZoneName: 'short',
|
|
22
|
+
}).format(date)
|
|
23
|
+
} catch {
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function buildCurrentDateTimePromptContext(preferredTimezone?: string | null): string {
|
|
29
|
+
const now = new Date()
|
|
30
|
+
const utcIso = now.toISOString()
|
|
31
|
+
const utcFormatted = formatDateTimeInTimezone(now, 'UTC') || utcIso
|
|
32
|
+
const localTimezone = resolveLocalTimezone()
|
|
33
|
+
const requestedTimezone = (preferredTimezone || '').trim()
|
|
34
|
+
const chosenTimezone = requestedTimezone || localTimezone
|
|
35
|
+
const chosenFormatted = formatDateTimeInTimezone(now, chosenTimezone)
|
|
36
|
+
|
|
37
|
+
const lines = [
|
|
38
|
+
'## Runtime Date/Time Context',
|
|
39
|
+
`- Current timestamp (UTC): ${utcIso}`,
|
|
40
|
+
`- Current date/time (UTC): ${utcFormatted}`,
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
if (chosenFormatted) {
|
|
44
|
+
lines.push(`- Current date/time (${chosenTimezone}): ${chosenFormatted}`)
|
|
45
|
+
} else if (requestedTimezone) {
|
|
46
|
+
lines.push(`- Requested timezone "${requestedTimezone}" could not be resolved. Use UTC time above.`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
lines.push('- Treat these as authoritative for terms like "today", "yesterday", "tomorrow", and "recent".')
|
|
50
|
+
lines.push('- For time-sensitive answers, use explicit dates (for example, "March 2, 2026").')
|
|
51
|
+
|
|
52
|
+
return lines.join('\n')
|
|
53
|
+
}
|