@swarmclawai/swarmclaw 0.7.8 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -15
- package/next.config.ts +13 -2
- package/package.json +4 -2
- package/src/app/api/agents/[id]/thread/route.ts +9 -0
- package/src/app/api/agents/route.ts +4 -0
- package/src/app/api/agents/thread-route.test.ts +133 -0
- package/src/app/api/approvals/route.test.ts +148 -0
- package/src/app/api/canvas/[sessionId]/route.ts +3 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
- package/src/app/api/chats/[id]/devserver/route.ts +48 -7
- package/src/app/api/chats/[id]/messages/route.ts +42 -18
- package/src/app/api/chats/[id]/route.ts +1 -1
- package/src/app/api/chats/[id]/stop/route.ts +5 -4
- package/src/app/api/chats/route.ts +22 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +26 -1
- package/src/app/api/external-agents/route.test.ts +165 -0
- package/src/app/api/gateways/[id]/health/route.ts +27 -12
- package/src/app/api/gateways/[id]/route.ts +2 -0
- package/src/app/api/gateways/health-route.test.ts +135 -0
- package/src/app/api/gateways/route.ts +2 -0
- package/src/app/api/mcp-servers/route.test.ts +130 -0
- package/src/app/api/openclaw/deploy/route.ts +38 -5
- package/src/app/api/plugins/install/route.ts +46 -6
- package/src/app/api/plugins/marketplace/route.ts +48 -15
- package/src/app/api/preview-server/route.ts +26 -11
- package/src/app/api/schedules/[id]/run/route.ts +4 -0
- package/src/app/api/schedules/route.test.ts +86 -0
- package/src/app/api/schedules/route.ts +6 -1
- package/src/app/api/setup/check-provider/route.test.ts +19 -0
- package/src/app/api/setup/check-provider/route.ts +40 -10
- package/src/app/api/skills/[id]/route.ts +12 -0
- package/src/app/api/skills/import/route.ts +14 -12
- package/src/app/api/skills/route.ts +13 -1
- package/src/app/api/tasks/[id]/route.ts +10 -1
- package/src/app/api/tasks/import/github/route.test.ts +65 -0
- package/src/app/api/tasks/import/github/route.ts +337 -0
- package/src/app/api/wallets/[id]/approve/route.ts +17 -3
- package/src/app/api/wallets/[id]/route.ts +79 -33
- package/src/app/api/wallets/[id]/send/route.ts +19 -33
- package/src/app/api/wallets/route.ts +78 -61
- package/src/app/api/webhooks/[id]/route.ts +33 -6
- package/src/app/api/webhooks/route.test.ts +272 -0
- package/src/cli/index.js +1 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-card.tsx +9 -2
- package/src/components/agents/agent-chat-list.tsx +18 -2
- package/src/components/agents/agent-list.tsx +1 -0
- package/src/components/agents/agent-sheet.tsx +73 -24
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +44 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- package/src/components/chat/message-bubble.tsx +208 -145
- package/src/components/chat/message-list.tsx +48 -19
- package/src/components/chatrooms/chatroom-message.tsx +2 -2
- package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
- package/src/components/connectors/connector-health.tsx +1 -1
- package/src/components/connectors/connector-list.tsx +7 -2
- package/src/components/connectors/connector-sheet.tsx +337 -148
- package/src/components/gateways/gateway-sheet.tsx +2 -2
- package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
- package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
- package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
- package/src/components/plugins/plugin-list.tsx +45 -9
- package/src/components/plugins/plugin-sheet.tsx +55 -7
- package/src/components/providers/provider-list.tsx +2 -1
- package/src/components/providers/provider-sheet.tsx +21 -2
- package/src/components/schedules/schedule-card.tsx +25 -1
- package/src/components/schedules/schedule-sheet.tsx +44 -2
- package/src/components/secrets/secret-sheet.tsx +21 -2
- package/src/components/shared/agent-switch-dialog.tsx +12 -1
- package/src/components/shared/bottom-sheet.tsx +13 -3
- package/src/components/shared/command-palette.tsx +8 -1
- package/src/components/shared/confirm-dialog.tsx +19 -4
- package/src/components/shared/connector-platform-icon.test.ts +28 -0
- package/src/components/shared/connector-platform-icon.tsx +39 -6
- package/src/components/shared/settings/plugin-manager.tsx +29 -6
- package/src/components/shared/settings/section-capability-policy.tsx +7 -3
- package/src/components/skills/skill-list.tsx +25 -0
- package/src/components/skills/skill-sheet.tsx +84 -12
- package/src/components/tasks/approvals-panel.tsx +191 -95
- package/src/components/tasks/task-board.tsx +273 -2
- package/src/components/tasks/task-card.tsx +38 -9
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
- package/src/components/wallets/wallet-panel.tsx +435 -90
- package/src/components/wallets/wallet-section.tsx +198 -48
- package/src/components/webhooks/webhook-sheet.tsx +22 -2
- package/src/lib/approval-display.ts +20 -0
- package/src/lib/canvas-content.ts +198 -0
- package/src/lib/chat-artifact-summary.ts +165 -0
- package/src/lib/chat-display.test.ts +91 -0
- package/src/lib/chat-display.ts +58 -0
- package/src/lib/chat-streaming-state.test.ts +47 -1
- package/src/lib/chat-streaming-state.ts +42 -0
- package/src/lib/ollama-model.ts +10 -0
- package/src/lib/openclaw-endpoint.test.ts +8 -0
- package/src/lib/openclaw-endpoint.ts +6 -1
- package/src/lib/plugin-install-cors.ts +46 -0
- package/src/lib/plugin-sources.test.ts +43 -0
- package/src/lib/plugin-sources.ts +77 -0
- package/src/lib/providers/ollama.ts +16 -6
- package/src/lib/providers/openclaw.test.ts +54 -0
- package/src/lib/providers/openclaw.ts +127 -11
- package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
- package/src/lib/schedule-dedupe.test.ts +66 -1
- package/src/lib/schedule-dedupe.ts +169 -12
- package/src/lib/schedule-origin.test.ts +20 -0
- package/src/lib/schedule-origin.ts +15 -0
- package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
- package/src/lib/server/agent-availability.ts +16 -0
- package/src/lib/server/agent-runtime-config.ts +12 -4
- package/src/lib/server/agent-thread-session.test.ts +51 -0
- package/src/lib/server/agent-thread-session.ts +7 -0
- package/src/lib/server/approval-match.ts +205 -0
- package/src/lib/server/approvals-auto-approve.test.ts +538 -1
- package/src/lib/server/approvals.ts +214 -1
- package/src/lib/server/assistant-control.test.ts +29 -0
- package/src/lib/server/assistant-control.ts +23 -0
- package/src/lib/server/build-llm.test.ts +79 -0
- package/src/lib/server/build-llm.ts +14 -4
- package/src/lib/server/canvas-content.test.ts +32 -0
- package/src/lib/server/canvas-content.ts +6 -0
- package/src/lib/server/capability-router.test.ts +11 -0
- package/src/lib/server/capability-router.ts +26 -1
- package/src/lib/server/chat-execution-advanced.test.ts +651 -0
- package/src/lib/server/chat-execution-disabled.test.ts +94 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
- package/src/lib/server/chat-execution.ts +353 -72
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +362 -63
- package/src/lib/server/connectors/pairing.ts +26 -5
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp.test.ts +134 -0
- package/src/lib/server/connectors/whatsapp.ts +271 -47
- package/src/lib/server/context-manager.ts +6 -1
- package/src/lib/server/daemon-state.ts +1 -1
- package/src/lib/server/data-dir.test.ts +37 -0
- package/src/lib/server/data-dir.ts +20 -1
- package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
- package/src/lib/server/devserver-launch.test.ts +60 -0
- package/src/lib/server/devserver-launch.ts +85 -0
- package/src/lib/server/elevenlabs.test.ts +189 -1
- package/src/lib/server/elevenlabs.ts +147 -43
- package/src/lib/server/ethereum.ts +590 -0
- package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
- package/src/lib/server/eval/agent-regression.test.ts +18 -1
- package/src/lib/server/eval/agent-regression.ts +383 -11
- package/src/lib/server/evm-swap.ts +475 -0
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
- package/src/lib/server/heartbeat-service.ts +15 -10
- package/src/lib/server/heartbeat-wake.test.ts +112 -0
- package/src/lib/server/heartbeat-wake.ts +338 -57
- package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
- package/src/lib/server/mcp-client.test.ts +16 -0
- package/src/lib/server/mcp-client.ts +25 -0
- package/src/lib/server/memory-integration.test.ts +719 -0
- package/src/lib/server/memory-policy.test.ts +43 -0
- package/src/lib/server/memory-policy.ts +132 -0
- package/src/lib/server/memory-tiers.test.ts +60 -0
- package/src/lib/server/memory-tiers.ts +16 -0
- package/src/lib/server/ollama-runtime.ts +58 -0
- package/src/lib/server/openclaw-deploy.test.ts +109 -1
- package/src/lib/server/openclaw-deploy.ts +557 -81
- package/src/lib/server/openclaw-gateway.test.ts +131 -0
- package/src/lib/server/openclaw-gateway.ts +10 -4
- package/src/lib/server/openclaw-health.test.ts +35 -0
- package/src/lib/server/openclaw-health.ts +215 -47
- package/src/lib/server/orchestrator-lg.ts +2 -2
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +205 -5
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +262 -0
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +293 -61
- package/src/lib/server/scheduler.ts +29 -1
- package/src/lib/server/session-note.test.ts +36 -0
- package/src/lib/server/session-note.ts +42 -0
- package/src/lib/server/session-run-manager.ts +52 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +348 -61
- package/src/lib/server/session-tools/context.ts +12 -3
- package/src/lib/server/session-tools/crud.ts +221 -10
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate.ts +64 -8
- package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
- package/src/lib/server/session-tools/discovery.ts +80 -12
- package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
- package/src/lib/server/session-tools/file.ts +43 -4
- package/src/lib/server/session-tools/human-loop.ts +35 -5
- package/src/lib/server/session-tools/index.ts +44 -9
- package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
- package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
- package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
- package/src/lib/server/session-tools/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +546 -79
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- package/src/lib/server/session-tools/plugin-creator.ts +57 -1
- package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
- package/src/lib/server/session-tools/schedule.ts +6 -1
- package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
- package/src/lib/server/session-tools/shell.ts +22 -3
- package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
- package/src/lib/server/session-tools/wallet.ts +1374 -139
- package/src/lib/server/session-tools/web-inputs.test.ts +162 -1
- package/src/lib/server/session-tools/web.ts +468 -64
- package/src/lib/server/skill-discovery.ts +128 -0
- package/src/lib/server/skill-eligibility.test.ts +84 -0
- package/src/lib/server/skill-eligibility.ts +95 -0
- package/src/lib/server/skill-prompt-budget.test.ts +102 -0
- package/src/lib/server/skill-prompt-budget.ts +125 -0
- package/src/lib/server/skills-normalize.test.ts +54 -0
- package/src/lib/server/skills-normalize.ts +372 -26
- package/src/lib/server/solana.ts +214 -29
- package/src/lib/server/storage.ts +65 -36
- package/src/lib/server/stream-agent-chat.test.ts +419 -9
- package/src/lib/server/stream-agent-chat.ts +887 -83
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- package/src/lib/server/tool-loop-detection.test.ts +105 -0
- package/src/lib/server/tool-loop-detection.ts +260 -0
- package/src/lib/server/tool-planning.ts +4 -2
- package/src/lib/server/wallet-execution.test.ts +198 -0
- package/src/lib/server/wallet-portfolio.test.ts +98 -0
- package/src/lib/server/wallet-portfolio.ts +724 -0
- package/src/lib/server/wallet-service.test.ts +57 -0
- package/src/lib/server/wallet-service.ts +213 -0
- package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
- package/src/lib/server/watch-jobs.ts +17 -2
- package/src/lib/server/workspace-context.ts +111 -0
- package/src/lib/skill-save-payload.test.ts +39 -0
- package/src/lib/skill-save-payload.ts +37 -0
- package/src/lib/tasks.ts +28 -0
- package/src/lib/tool-event-summary.test.ts +30 -0
- package/src/lib/tool-event-summary.ts +37 -0
- package/src/lib/validation/schemas.ts +1 -0
- package/src/lib/wallet-transactions.test.ts +75 -0
- package/src/lib/wallet-transactions.ts +43 -0
- package/src/lib/wallet.test.ts +17 -0
- package/src/lib/wallet.ts +183 -0
- package/src/proxy.test.ts +31 -0
- package/src/proxy.ts +34 -2
- package/src/stores/use-chat-store.ts +15 -1
- package/src/types/index.ts +210 -14
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import {
|
|
4
|
+
inferAutomaticMemoryCategory,
|
|
5
|
+
normalizeMemoryCategory,
|
|
6
|
+
shouldAutoCaptureMemory,
|
|
7
|
+
shouldInjectMemoryContext,
|
|
8
|
+
} from './memory-policy'
|
|
9
|
+
|
|
10
|
+
test('normalizeMemoryCategory maps flat categories into hierarchical buckets', () => {
|
|
11
|
+
assert.equal(normalizeMemoryCategory('preference', 'User prefers terse replies'), 'identity/preferences')
|
|
12
|
+
assert.equal(normalizeMemoryCategory('decision', 'Ship the Docker path'), 'projects/decisions')
|
|
13
|
+
assert.equal(normalizeMemoryCategory('error', 'Root cause found'), 'execution/errors')
|
|
14
|
+
assert.equal(normalizeMemoryCategory('project', 'Repo setup'), 'projects/context')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('shouldInjectMemoryContext skips low-signal greetings and acknowledgements', () => {
|
|
18
|
+
assert.equal(shouldInjectMemoryContext('thanks'), false)
|
|
19
|
+
assert.equal(shouldInjectMemoryContext('hello'), false)
|
|
20
|
+
assert.equal(shouldInjectMemoryContext('Remember this for later'), false)
|
|
21
|
+
assert.equal(shouldInjectMemoryContext('Compare the current deployment plan with what we decided yesterday'), true)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('shouldAutoCaptureMemory filters noisy turns', () => {
|
|
25
|
+
assert.equal(shouldAutoCaptureMemory({ message: 'thanks', response: 'Happy to help with that.', source: 'chat' }), false)
|
|
26
|
+
assert.equal(shouldAutoCaptureMemory({ message: 'Please save this to memory', response: 'Stored memory "note".', source: 'chat' }), false)
|
|
27
|
+
assert.equal(shouldAutoCaptureMemory({
|
|
28
|
+
message: 'We decided to use the shared staging environment and keep the worker count at 2 for now.',
|
|
29
|
+
response: 'Decision captured: shared staging, worker count 2, and we will revisit after load testing next week.',
|
|
30
|
+
source: 'chat',
|
|
31
|
+
}), true)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('inferAutomaticMemoryCategory picks a stable automatic bucket', () => {
|
|
35
|
+
assert.equal(
|
|
36
|
+
inferAutomaticMemoryCategory('The user prefers direct status updates.', 'I will keep future updates terse and direct.'),
|
|
37
|
+
'identity/preferences',
|
|
38
|
+
)
|
|
39
|
+
assert.equal(
|
|
40
|
+
inferAutomaticMemoryCategory('We decided to ship the GitHub import first.', 'Decision locked for the next milestone.'),
|
|
41
|
+
'projects/decisions',
|
|
42
|
+
)
|
|
43
|
+
})
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { MemoryEntry } from '@/types'
|
|
2
|
+
|
|
3
|
+
const ACK_RE = /^(?:ok(?:ay)?|cool|nice|got it|makes sense|thanks|thank you|thx|roger|copy|sounds good|sgtm|yep|yup|y|nope?|nah|kk|done)[.! ]*$/i
|
|
4
|
+
const GREETING_RE = /^(?:hi|hello|hey|yo|morning|good morning|good afternoon|good evening)[.! ]*$/i
|
|
5
|
+
const MEMORY_META_RE = /\b(?:remember|memory|memorize|store this|save this|forget)\b/i
|
|
6
|
+
const LOW_SIGNAL_RESPONSE_RE = /^(?:HEARTBEAT_OK|NO_MESSAGE)\b/i
|
|
7
|
+
|
|
8
|
+
function normalizeWhitespace(value: string): string {
|
|
9
|
+
return value.replace(/\s+/g, ' ').trim()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function lower(value: string | null | undefined): string {
|
|
13
|
+
return normalizeWhitespace(value || '').toLowerCase()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function shouldInjectMemoryContext(message: string): boolean {
|
|
17
|
+
const trimmed = normalizeWhitespace(message)
|
|
18
|
+
if (!trimmed) return false
|
|
19
|
+
if (trimmed.length < 16 && (ACK_RE.test(trimmed) || GREETING_RE.test(trimmed))) return false
|
|
20
|
+
if (trimmed.length < 24 && MEMORY_META_RE.test(trimmed)) return false
|
|
21
|
+
return true
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function shouldAutoCaptureMemoryTurn(message: string, response: string): boolean {
|
|
25
|
+
const normalizedMessage = normalizeWhitespace(message)
|
|
26
|
+
const normalizedResponse = normalizeWhitespace(response)
|
|
27
|
+
if (normalizedMessage.length < 20 || normalizedResponse.length < 40) return false
|
|
28
|
+
if (ACK_RE.test(normalizedMessage) || GREETING_RE.test(normalizedMessage)) return false
|
|
29
|
+
if (LOW_SIGNAL_RESPONSE_RE.test(normalizedResponse)) return false
|
|
30
|
+
if (MEMORY_META_RE.test(normalizedMessage) && normalizedMessage.length < 120) return false
|
|
31
|
+
if (/^(?:sorry|i can(?:not|'t)|unable to|i do not have|i don't have)\b/i.test(normalizedResponse)) return false
|
|
32
|
+
return true
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function shouldAutoCaptureMemory(
|
|
36
|
+
input: { message?: string | null; response?: string | null } | string,
|
|
37
|
+
response?: string,
|
|
38
|
+
): boolean {
|
|
39
|
+
if (typeof input === 'string') {
|
|
40
|
+
return shouldAutoCaptureMemoryTurn(input, response || '')
|
|
41
|
+
}
|
|
42
|
+
return shouldAutoCaptureMemoryTurn(input.message || '', input.response || '')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function normalizeMemoryCategory(
|
|
46
|
+
input: string | null | undefined,
|
|
47
|
+
title: string | null | undefined,
|
|
48
|
+
content: string | null | undefined,
|
|
49
|
+
): string {
|
|
50
|
+
const explicit = lower(input)
|
|
51
|
+
const sample = `${lower(title)}\n${lower(content)}`
|
|
52
|
+
|
|
53
|
+
const mapExplicit = (value: string): string | null => {
|
|
54
|
+
if (!value || value === 'note' || value === 'notes') return null
|
|
55
|
+
if (['preference', 'preferences', 'likes', 'dislikes'].includes(value)) return 'identity/preferences'
|
|
56
|
+
if (['identity', 'profile', 'persona'].includes(value)) return 'identity/profile'
|
|
57
|
+
if (['relationship', 'relationships', 'people'].includes(value)) return 'identity/relationships'
|
|
58
|
+
if (['decision', 'decisions', 'choice'].includes(value)) return 'projects/decisions'
|
|
59
|
+
if (['learning', 'learnings', 'lesson', 'lessons'].includes(value)) return 'projects/learnings'
|
|
60
|
+
if (['project', 'projects', 'task', 'tasks'].includes(value)) return 'projects/context'
|
|
61
|
+
if (['error', 'errors', 'incident', 'incidents', 'failure', 'failures'].includes(value)) return 'execution/errors'
|
|
62
|
+
if (['breadcrumb', 'execution', 'run', 'runs'].includes(value)) return 'operations/execution'
|
|
63
|
+
if (['fact', 'facts', 'knowledge', 'reference'].includes(value)) return 'knowledge/facts'
|
|
64
|
+
if (['working', 'scratch', 'draft'].includes(value)) return 'working/scratch'
|
|
65
|
+
if (value.includes('/')) return value
|
|
66
|
+
return value
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const explicitMapped = mapExplicit(explicit)
|
|
70
|
+
if (explicitMapped) return explicitMapped
|
|
71
|
+
|
|
72
|
+
if (/\b(?:prefer(?:s|ence)?|likes?|dislikes?|favorite|timezone|pronouns|call me)\b/.test(sample)) {
|
|
73
|
+
return 'identity/preferences'
|
|
74
|
+
}
|
|
75
|
+
if (/\b(?:wife|husband|partner|friend|manager|teammate|client|customer|relationship)\b/.test(sample)) {
|
|
76
|
+
return 'identity/relationships'
|
|
77
|
+
}
|
|
78
|
+
if (/\b(?:decided|decision|approved|picked|selected|going with|will use)\b/.test(sample)) {
|
|
79
|
+
return 'projects/decisions'
|
|
80
|
+
}
|
|
81
|
+
if (/\b(?:learned|lesson|fixed|solved|root cause|failure|bug|regression|postmortem)\b/.test(sample)) {
|
|
82
|
+
return 'projects/learnings'
|
|
83
|
+
}
|
|
84
|
+
if (/\b(?:error|incident|stack trace|exception|crash)\b/.test(sample)) {
|
|
85
|
+
return 'execution/errors'
|
|
86
|
+
}
|
|
87
|
+
if (/\b(?:project|repo|repository|ticket|task|milestone|deadline|roadmap)\b/.test(sample)) {
|
|
88
|
+
return 'projects/context'
|
|
89
|
+
}
|
|
90
|
+
if (/\b(?:config|credential|endpoint|workspace|path|env var|environment|docker|sandbox)\b/.test(sample)) {
|
|
91
|
+
return 'operations/environment'
|
|
92
|
+
}
|
|
93
|
+
if (/\b(?:fact|documentation|reference|api|schema)\b/.test(sample)) {
|
|
94
|
+
return 'knowledge/facts'
|
|
95
|
+
}
|
|
96
|
+
return explicit && explicit !== 'note' && explicit !== 'notes' ? explicit : 'knowledge/facts'
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function buildMemoryDoctorReport(entries: MemoryEntry[], agentId?: string | null): string {
|
|
100
|
+
const topLevelCounts = new Map<string, number>()
|
|
101
|
+
let pinned = 0
|
|
102
|
+
let linked = 0
|
|
103
|
+
let shared = 0
|
|
104
|
+
|
|
105
|
+
for (const entry of entries) {
|
|
106
|
+
const category = normalizeMemoryCategory(entry.category, entry.title, entry.content)
|
|
107
|
+
const topLevel = category.split('/')[0] || 'other'
|
|
108
|
+
topLevelCounts.set(topLevel, (topLevelCounts.get(topLevel) || 0) + 1)
|
|
109
|
+
if (entry.pinned) pinned += 1
|
|
110
|
+
if (entry.linkedMemoryIds?.length) linked += 1
|
|
111
|
+
if (entry.sharedWith?.length) shared += 1
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const categories = [...topLevelCounts.entries()]
|
|
115
|
+
.sort((left, right) => right[1] - left[1])
|
|
116
|
+
.map(([name, count]) => `- ${name}: ${count}`)
|
|
117
|
+
|
|
118
|
+
return [
|
|
119
|
+
'Memory Doctor',
|
|
120
|
+
`Agent scope: ${agentId || 'global/all'}`,
|
|
121
|
+
`Visible memories: ${entries.length}`,
|
|
122
|
+
`Pinned: ${pinned}`,
|
|
123
|
+
`Linked: ${linked}`,
|
|
124
|
+
`Shared: ${shared}`,
|
|
125
|
+
categories.length ? 'Top-level categories:' : 'Top-level categories: none',
|
|
126
|
+
...(categories.length ? categories : []),
|
|
127
|
+
].join('\n')
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function inferAutomaticMemoryCategory(message: string, response: string): string {
|
|
131
|
+
return normalizeMemoryCategory('note', message, response)
|
|
132
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import type { MemoryEntry } from '@/types'
|
|
4
|
+
|
|
5
|
+
import { getMemoryTierForCategory, partitionMemoriesByTier, shouldHideFromDurableRecall } from './memory-tiers'
|
|
6
|
+
|
|
7
|
+
test('getMemoryTierForCategory treats hierarchical execution and archive categories correctly', () => {
|
|
8
|
+
assert.equal(getMemoryTierForCategory('operations/execution'), 'working')
|
|
9
|
+
assert.equal(getMemoryTierForCategory('operations/execution/tool-run'), 'working')
|
|
10
|
+
assert.equal(getMemoryTierForCategory('working/scratch'), 'working')
|
|
11
|
+
assert.equal(getMemoryTierForCategory('session_archive'), 'archive')
|
|
12
|
+
assert.equal(getMemoryTierForCategory('projects/decisions'), 'durable')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('partitionMemoriesByTier keeps auto execution notes out of durable memory', () => {
|
|
16
|
+
const partitioned = partitionMemoriesByTier([
|
|
17
|
+
{ category: 'projects/decisions', metadata: undefined },
|
|
18
|
+
{ category: 'operations/execution', metadata: undefined },
|
|
19
|
+
{ category: 'session_archive', metadata: { tier: 'archive' } },
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
assert.equal(partitioned.durable.length, 1)
|
|
23
|
+
assert.equal(partitioned.working.length, 1)
|
|
24
|
+
assert.equal(partitioned.archive.length, 1)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('shouldHideFromDurableRecall hides superseded and auto-consolidated entries', () => {
|
|
28
|
+
const autoConsolidatedTitle: Pick<MemoryEntry, 'title' | 'metadata'> = {
|
|
29
|
+
title: '[auto-consolidated] Project Kodiak note',
|
|
30
|
+
metadata: undefined,
|
|
31
|
+
}
|
|
32
|
+
const autoConsolidatedOrigin: Pick<MemoryEntry, 'title' | 'metadata'> = {
|
|
33
|
+
title: 'Project Kodiak',
|
|
34
|
+
metadata: { origin: 'auto-consolidated' },
|
|
35
|
+
}
|
|
36
|
+
const supersededEntry: Pick<MemoryEntry, 'title' | 'metadata'> = {
|
|
37
|
+
title: 'Project Kodiak',
|
|
38
|
+
metadata: { supersededBy: 'abc123' },
|
|
39
|
+
}
|
|
40
|
+
const canonicalEntry: Pick<MemoryEntry, 'title' | 'metadata'> = {
|
|
41
|
+
title: 'Canonical project fact',
|
|
42
|
+
metadata: {},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
assert.equal(shouldHideFromDurableRecall({
|
|
46
|
+
...autoConsolidatedTitle,
|
|
47
|
+
}), true)
|
|
48
|
+
|
|
49
|
+
assert.equal(shouldHideFromDurableRecall({
|
|
50
|
+
...autoConsolidatedOrigin,
|
|
51
|
+
}), true)
|
|
52
|
+
|
|
53
|
+
assert.equal(shouldHideFromDurableRecall({
|
|
54
|
+
...supersededEntry,
|
|
55
|
+
}), true)
|
|
56
|
+
|
|
57
|
+
assert.equal(shouldHideFromDurableRecall({
|
|
58
|
+
...canonicalEntry,
|
|
59
|
+
}), false)
|
|
60
|
+
})
|
|
@@ -8,6 +8,10 @@ const ARCHIVE_CATEGORIES = new Set(['session_archive'])
|
|
|
8
8
|
export function getMemoryTierForCategory(category: unknown): MemoryTier {
|
|
9
9
|
const normalized = typeof category === 'string' ? category.trim().toLowerCase() : ''
|
|
10
10
|
if (ARCHIVE_CATEGORIES.has(normalized)) return 'archive'
|
|
11
|
+
if (normalized.startsWith('session_archive/')) return 'archive'
|
|
12
|
+
if (normalized === 'operations/execution' || normalized.startsWith('operations/execution/')) return 'working'
|
|
13
|
+
if (normalized === 'working/scratch' || normalized.startsWith('working/')) return 'working'
|
|
14
|
+
if (normalized === 'execution' || normalized.startsWith('execution/')) return 'working'
|
|
11
15
|
if (WORKING_CATEGORIES.has(normalized)) return 'working'
|
|
12
16
|
return 'durable'
|
|
13
17
|
}
|
|
@@ -38,3 +42,15 @@ export function partitionMemoriesByTier<T extends Pick<MemoryEntry, 'category' |
|
|
|
38
42
|
export function isWorkingMemoryCategory(category: unknown): boolean {
|
|
39
43
|
return getMemoryTierForCategory(category) === 'working'
|
|
40
44
|
}
|
|
45
|
+
|
|
46
|
+
export function shouldHideFromDurableRecall(
|
|
47
|
+
entry: Pick<MemoryEntry, 'title' | 'metadata'>,
|
|
48
|
+
): boolean {
|
|
49
|
+
const metadata = entry.metadata || {}
|
|
50
|
+
const origin = typeof metadata.origin === 'string' ? metadata.origin.trim().toLowerCase() : ''
|
|
51
|
+
if (origin === 'auto-consolidated') return true
|
|
52
|
+
if (typeof metadata.supersededBy === 'string' && metadata.supersededBy.trim()) return true
|
|
53
|
+
if (typeof metadata.supersededAt === 'number' && Number.isFinite(metadata.supersededAt)) return true
|
|
54
|
+
const title = typeof entry.title === 'string' ? entry.title.trim().toLowerCase() : ''
|
|
55
|
+
return title.startsWith('[auto-consolidated]')
|
|
56
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { isOllamaCloudModel, stripOllamaCloudModelSuffix } from '@/lib/ollama-model'
|
|
2
|
+
import { PROVIDER_DEFAULTS } from '@/lib/providers/provider-defaults'
|
|
3
|
+
|
|
4
|
+
const OLLAMA_CLOUD_KEY_ENV_VARS = ['OLLAMA_API_KEY', 'OLLAMA_CLOUD_API_KEY'] as const
|
|
5
|
+
|
|
6
|
+
function clean(value: string | null | undefined): string | null {
|
|
7
|
+
if (typeof value !== 'string') return null
|
|
8
|
+
const trimmed = value.trim()
|
|
9
|
+
return trimmed || null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isOllamaCloudEndpoint(endpoint: string | null | undefined): boolean {
|
|
13
|
+
const normalized = clean(endpoint)
|
|
14
|
+
if (!normalized) return false
|
|
15
|
+
return /^https?:\/\/(?:www\.)?ollama\.com(?:\/|$)/i.test(normalized)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function hasExplicitEndpoint(endpoint: string | null | undefined): boolean {
|
|
19
|
+
return clean(endpoint) !== null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resolveOllamaCloudApiKey(explicitApiKey?: string | null): string | null {
|
|
23
|
+
const explicit = clean(explicitApiKey)
|
|
24
|
+
if (explicit && explicit !== 'ollama') return explicit
|
|
25
|
+
for (const envName of OLLAMA_CLOUD_KEY_ENV_VARS) {
|
|
26
|
+
const candidate = clean(process.env[envName])
|
|
27
|
+
if (candidate) return candidate
|
|
28
|
+
}
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function resolveOllamaRuntimeConfig(input: {
|
|
33
|
+
model?: string | null
|
|
34
|
+
apiKey?: string | null
|
|
35
|
+
apiEndpoint?: string | null
|
|
36
|
+
}): {
|
|
37
|
+
model: string
|
|
38
|
+
useCloud: boolean
|
|
39
|
+
apiKey: string | null
|
|
40
|
+
endpoint: string
|
|
41
|
+
} {
|
|
42
|
+
const rawModel = clean(input.model) || ''
|
|
43
|
+
const explicitApiKey = clean(input.apiKey)
|
|
44
|
+
const explicitEndpoint = clean(input.apiEndpoint)
|
|
45
|
+
const cloudApiKey = resolveOllamaCloudApiKey(explicitApiKey)
|
|
46
|
+
const useCloud = isOllamaCloudEndpoint(explicitEndpoint)
|
|
47
|
+
|| (!hasExplicitEndpoint(explicitEndpoint) && (
|
|
48
|
+
Boolean(explicitApiKey && explicitApiKey !== 'ollama')
|
|
49
|
+
|| (isOllamaCloudModel(rawModel) && Boolean(cloudApiKey))
|
|
50
|
+
))
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
model: useCloud ? (stripOllamaCloudModelSuffix(rawModel) || rawModel) : rawModel,
|
|
54
|
+
useCloud,
|
|
55
|
+
apiKey: useCloud ? cloudApiKey : explicitApiKey,
|
|
56
|
+
endpoint: useCloud ? PROVIDER_DEFAULTS.ollamaCloud : (explicitEndpoint || PROVIDER_DEFAULTS.ollama),
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
|
-
import { test } from 'node:test'
|
|
2
|
+
import { afterEach, test } from 'node:test'
|
|
3
3
|
import {
|
|
4
4
|
buildOpenClawDeployBundle,
|
|
5
|
+
getOpenClawLocalDeployCollectionStatus,
|
|
5
6
|
getOpenClawLocalDeployStatus,
|
|
7
|
+
getOpenClawRemoteDeployCollectionStatus,
|
|
8
|
+
getOpenClawRemoteDeployStatus,
|
|
6
9
|
} from './openclaw-deploy.ts'
|
|
7
10
|
|
|
11
|
+
const GLOBAL_KEY = '__swarmclaw_openclaw_deploy__' as const
|
|
12
|
+
const originalRuntimeState = (globalThis as typeof globalThis & { [GLOBAL_KEY]?: unknown })[GLOBAL_KEY]
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
;(globalThis as typeof globalThis & { [GLOBAL_KEY]?: unknown })[GLOBAL_KEY] = originalRuntimeState
|
|
16
|
+
})
|
|
17
|
+
|
|
8
18
|
test('docker smart deploy bundle uses official image and provider-specific metadata', () => {
|
|
9
19
|
const bundle = buildOpenClawDeployBundle({
|
|
10
20
|
template: 'docker',
|
|
@@ -66,10 +76,108 @@ test('render bundle stays aligned with the official repo flow', () => {
|
|
|
66
76
|
|
|
67
77
|
test('local deploy status exposes a sensible default endpoint before startup', () => {
|
|
68
78
|
const status = getOpenClawLocalDeployStatus()
|
|
79
|
+
const collection = getOpenClawLocalDeployCollectionStatus()
|
|
69
80
|
|
|
81
|
+
assert.equal(status.id, 'local-default')
|
|
82
|
+
assert.equal(status.isPrimary, true)
|
|
70
83
|
assert.equal(status.running, false)
|
|
71
84
|
assert.equal(status.port, 18789)
|
|
72
85
|
assert.equal(status.endpoint, 'http://127.0.0.1:18789/v1')
|
|
73
86
|
assert.equal(status.wsUrl, 'ws://127.0.0.1:18789')
|
|
74
87
|
assert.match(status.launchCommand, /npx openclaw gateway run/)
|
|
88
|
+
assert.equal(collection.primaryId, null)
|
|
89
|
+
assert.deepEqual(collection.items, [])
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('remote deploy status exposes a sensible default record before startup', () => {
|
|
93
|
+
const status = getOpenClawRemoteDeployStatus()
|
|
94
|
+
const collection = getOpenClawRemoteDeployCollectionStatus()
|
|
95
|
+
|
|
96
|
+
assert.equal(status.id, 'remote-default')
|
|
97
|
+
assert.equal(status.name, 'Remote OpenClaw')
|
|
98
|
+
assert.equal(status.isPrimary, true)
|
|
99
|
+
assert.equal(status.active, false)
|
|
100
|
+
assert.equal(status.status, 'idle')
|
|
101
|
+
assert.equal(status.target, null)
|
|
102
|
+
assert.equal(collection.primaryId, null)
|
|
103
|
+
assert.deepEqual(collection.items, [])
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('legacy singleton remote runtime state is migrated into the keyed remote collection', () => {
|
|
107
|
+
;(globalThis as typeof globalThis & { [GLOBAL_KEY]?: unknown })[GLOBAL_KEY] = {
|
|
108
|
+
locals: {},
|
|
109
|
+
primaryLocalId: null,
|
|
110
|
+
remote: {
|
|
111
|
+
processId: null,
|
|
112
|
+
action: 'ssh-deploy',
|
|
113
|
+
target: 'gateway.example.com',
|
|
114
|
+
startedAt: 123,
|
|
115
|
+
lastError: null,
|
|
116
|
+
lastSummary: 'Deploying OpenClaw to gateway.example.com over SSH.',
|
|
117
|
+
lastCommandPreview: 'ssh root@gateway.example.com ...',
|
|
118
|
+
lastBackupPath: null,
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const status = getOpenClawRemoteDeployStatus()
|
|
123
|
+
const collection = getOpenClawRemoteDeployCollectionStatus()
|
|
124
|
+
|
|
125
|
+
assert.equal(status.id, 'remote-default')
|
|
126
|
+
assert.equal(status.name, 'gateway.example.com')
|
|
127
|
+
assert.equal(status.target, 'gateway.example.com')
|
|
128
|
+
assert.equal(status.action, 'ssh-deploy')
|
|
129
|
+
assert.equal(status.lastSummary, 'Deploying OpenClaw to gateway.example.com over SSH.')
|
|
130
|
+
assert.equal(collection.primaryId, 'remote-default')
|
|
131
|
+
assert.equal(collection.items.length, 1)
|
|
132
|
+
assert.equal(collection.items[0]?.id, 'remote-default')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('remote deploy collection preserves multiple remotes and targeted lookup', () => {
|
|
136
|
+
;(globalThis as typeof globalThis & { [GLOBAL_KEY]?: unknown })[GLOBAL_KEY] = {
|
|
137
|
+
locals: {},
|
|
138
|
+
primaryLocalId: null,
|
|
139
|
+
remotes: {
|
|
140
|
+
'remote-alpha': {
|
|
141
|
+
name: 'alpha',
|
|
142
|
+
processId: null,
|
|
143
|
+
action: 'restart',
|
|
144
|
+
target: 'alpha.example.com',
|
|
145
|
+
startedAt: null,
|
|
146
|
+
createdAt: 10,
|
|
147
|
+
updatedAt: 20,
|
|
148
|
+
lastError: null,
|
|
149
|
+
lastSummary: 'Restarting OpenClaw on alpha.example.com.',
|
|
150
|
+
lastCommandPreview: 'ssh root@alpha.example.com docker compose restart',
|
|
151
|
+
lastBackupPath: null,
|
|
152
|
+
},
|
|
153
|
+
'remote-beta': {
|
|
154
|
+
name: 'beta',
|
|
155
|
+
processId: null,
|
|
156
|
+
action: 'upgrade',
|
|
157
|
+
target: 'beta.example.com',
|
|
158
|
+
startedAt: null,
|
|
159
|
+
createdAt: 30,
|
|
160
|
+
updatedAt: 40,
|
|
161
|
+
lastError: null,
|
|
162
|
+
lastSummary: 'Pulling openclaw:latest and recreating the OpenClaw stack on beta.example.com.',
|
|
163
|
+
lastCommandPreview: 'ssh root@beta.example.com docker compose up -d',
|
|
164
|
+
lastBackupPath: null,
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
primaryRemoteId: 'remote-alpha',
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const collection = getOpenClawRemoteDeployCollectionStatus()
|
|
171
|
+
const primary = getOpenClawRemoteDeployStatus()
|
|
172
|
+
const beta = getOpenClawRemoteDeployStatus('remote-beta')
|
|
173
|
+
|
|
174
|
+
assert.equal(collection.primaryId, 'remote-alpha')
|
|
175
|
+
assert.equal(collection.items.length, 2)
|
|
176
|
+
assert.equal(collection.items[0]?.id, 'remote-beta')
|
|
177
|
+
assert.equal(collection.items[1]?.id, 'remote-alpha')
|
|
178
|
+
assert.equal(primary.id, 'remote-alpha')
|
|
179
|
+
assert.equal(primary.isPrimary, true)
|
|
180
|
+
assert.equal(beta.id, 'remote-beta')
|
|
181
|
+
assert.equal(beta.target, 'beta.example.com')
|
|
182
|
+
assert.equal(beta.isPrimary, false)
|
|
75
183
|
})
|