@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
|
@@ -8,30 +8,321 @@ import {
|
|
|
8
8
|
getMemoryLookupLimits,
|
|
9
9
|
normalizeMemoryScopeMode,
|
|
10
10
|
storeMemoryImageAsset,
|
|
11
|
+
type MemoryScopeFilter,
|
|
11
12
|
} from '../memory-db'
|
|
12
13
|
import { loadSettings } from '../storage'
|
|
13
14
|
import { expandQuery } from '../query-expansion'
|
|
14
|
-
import type { MemoryEntry, Plugin, PluginHooks } from '@/types'
|
|
15
|
+
import type { FileReference, MemoryEntry, MemoryImage, MemoryReference, Plugin, PluginHooks, Session } from '@/types'
|
|
15
16
|
import type { ToolBuildContext } from './context'
|
|
16
17
|
import { getPluginManager } from '../plugins'
|
|
17
18
|
import { normalizeToolInputArgs } from './normalize-tool-args'
|
|
18
|
-
import { partitionMemoriesByTier } from '../memory-tiers'
|
|
19
|
+
import { getMemoryTier, partitionMemoriesByTier, shouldHideFromDurableRecall } from '../memory-tiers'
|
|
19
20
|
import { syncSessionArchiveMemory } from '../session-archive-memory'
|
|
21
|
+
import {
|
|
22
|
+
buildMemoryDoctorReport,
|
|
23
|
+
normalizeMemoryCategory,
|
|
24
|
+
shouldAutoCaptureMemoryTurn,
|
|
25
|
+
shouldInjectMemoryContext,
|
|
26
|
+
} from '../memory-policy'
|
|
20
27
|
|
|
21
28
|
/**
|
|
22
29
|
* Advanced Database-Backed Memory logic.
|
|
23
30
|
*/
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
type MemoryActionContext = Partial<Session> & {
|
|
32
|
+
sessionId?: string | null
|
|
33
|
+
memoryScopeMode?: string | null
|
|
34
|
+
projectRoot?: string | null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type MemorySearchSource = 'durable' | 'working' | 'archive' | 'all'
|
|
38
|
+
type CanonicalMemoryCandidate = {
|
|
39
|
+
entry: MemoryEntry
|
|
40
|
+
score: number
|
|
41
|
+
sharedTokens: number
|
|
42
|
+
overlap: number
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const MEMORY_SUBJECT_STOP_WORDS = new Set([
|
|
46
|
+
'a', 'an', 'and', 'assistant', 'current', 'details', 'fact', 'facts', 'for',
|
|
47
|
+
'from', 'got', 'have', 'i', 'in', 'is', 'it', 'its', 'ive', 'memory', 'my',
|
|
48
|
+
'note', 'notes', 'of', 'our', 'project', 'remember', 'stored', 'storing',
|
|
49
|
+
'that', 'the', 'this', 'to', 'updated', 'updating', 'with', 'you', 'your',
|
|
50
|
+
])
|
|
51
|
+
|
|
52
|
+
const MEMORY_VOLATILE_STOP_WORDS = new Set([
|
|
53
|
+
'april', 'august', 'corrected', 'correction', 'date', 'dates', 'december',
|
|
54
|
+
'earlier', 'error', 'february', 'freeze', 'january', 'july', 'june', 'march',
|
|
55
|
+
'may', 'new', 'november', 'october', 'old', 'september',
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
function isSessionContext(ctx: MemoryActionContext | null | undefined): ctx is Session {
|
|
59
|
+
return !!ctx
|
|
60
|
+
&& typeof ctx.id === 'string'
|
|
61
|
+
&& typeof ctx.name === 'string'
|
|
62
|
+
&& Array.isArray(ctx.messages)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function latestUserFactFromSession(session: Session | null): string {
|
|
66
|
+
if (!session || !Array.isArray(session.messages)) return ''
|
|
67
|
+
for (let index = session.messages.length - 1; index >= 0; index--) {
|
|
68
|
+
const message = session.messages[index]
|
|
69
|
+
if (message?.role !== 'user') continue
|
|
70
|
+
const text = typeof message.text === 'string' ? message.text.replace(/\s+/g, ' ').trim() : ''
|
|
71
|
+
if (text) return text
|
|
72
|
+
}
|
|
73
|
+
return ''
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function normalizeMemorySearchSources(raw: unknown): Set<MemorySearchSource> {
|
|
77
|
+
const sources = Array.isArray(raw) ? raw : []
|
|
78
|
+
const normalized = new Set<MemorySearchSource>()
|
|
79
|
+
for (const entry of sources) {
|
|
80
|
+
const value = typeof entry === 'string' ? entry.trim().toLowerCase() : ''
|
|
81
|
+
if (value === 'all') normalized.add('all')
|
|
82
|
+
else if (value === 'durable' || value === 'working' || value === 'archive') normalized.add(value)
|
|
83
|
+
}
|
|
84
|
+
if (normalized.size === 0) normalized.add('durable')
|
|
85
|
+
if (normalized.has('all')) return new Set<MemorySearchSource>(['all'])
|
|
86
|
+
return normalized
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function parseStructuredMemoryRecord(raw: unknown): Record<string, unknown> | null {
|
|
90
|
+
if (!raw) return null
|
|
91
|
+
if (typeof raw === 'object' && !Array.isArray(raw)) return raw as Record<string, unknown>
|
|
92
|
+
if (typeof raw !== 'string') return null
|
|
93
|
+
const trimmed = raw.trim()
|
|
94
|
+
if (!trimmed.startsWith('{')) return null
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(trimmed)
|
|
97
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
98
|
+
? parsed as Record<string, unknown>
|
|
99
|
+
: null
|
|
100
|
+
} catch {
|
|
101
|
+
return null
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizeStructuredMemoryArgs(raw: Record<string, unknown>): Record<string, unknown> {
|
|
106
|
+
const normalized = { ...raw }
|
|
107
|
+
for (const key of ['value', 'query', 'key', 'input', 'data', 'payload', 'parameters'] as const) {
|
|
108
|
+
const parsed = parseStructuredMemoryRecord(normalized[key])
|
|
109
|
+
if (!parsed) continue
|
|
110
|
+
for (const [nestedKey, nestedValue] of Object.entries(parsed)) {
|
|
111
|
+
if (normalized[nestedKey] === undefined || normalized[nestedKey] === null || normalized[nestedKey] === '') {
|
|
112
|
+
normalized[nestedKey] = nestedValue
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if ((normalized.value === undefined || normalized.value === null || normalized.value === '')
|
|
116
|
+
&& typeof parsed.content === 'string') {
|
|
117
|
+
normalized.value = parsed.content
|
|
118
|
+
}
|
|
119
|
+
if ((normalized.title === undefined || normalized.title === null || normalized.title === '')
|
|
120
|
+
&& typeof parsed.name === 'string') {
|
|
121
|
+
normalized.title = parsed.name
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (normalized.value === undefined || normalized.value === null || normalized.value === '') {
|
|
125
|
+
for (const alias of ['content', 'note', 'body', 'text', 'memory'] as const) {
|
|
126
|
+
if (typeof normalized[alias] === 'string' && normalized[alias].trim()) {
|
|
127
|
+
normalized.value = normalized[alias]
|
|
128
|
+
break
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return normalized
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function filterResultsBySources(entries: MemoryEntry[], sources: Set<MemorySearchSource>): MemoryEntry[] {
|
|
136
|
+
if (sources.has('all')) return entries
|
|
137
|
+
return entries.filter((entry) => {
|
|
138
|
+
const tier = getMemoryTier(entry)
|
|
139
|
+
if (!sources.has(tier)) return false
|
|
140
|
+
if (tier === 'durable' && shouldHideFromDurableRecall(entry)) return false
|
|
141
|
+
return true
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function normalizeMemoryText(value: unknown): string {
|
|
146
|
+
return String(value || '')
|
|
147
|
+
.toLowerCase()
|
|
148
|
+
.replace(/\s+/g, ' ')
|
|
149
|
+
.replace(/[^\w\s:/.-]/g, '')
|
|
150
|
+
.trim()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function stripGeneratedMemoryPrefix(value: string): string {
|
|
154
|
+
return value.replace(/^\[(?:auto|auto-consolidated)[^\]]*\]\s*/i, '').trim()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function tokenizeMemorySubject(value: string): string[] {
|
|
158
|
+
const tokens = normalizeMemoryText(value).match(/[a-z0-9][a-z0-9._:/-]*/g) || []
|
|
159
|
+
const out: string[] = []
|
|
160
|
+
const seen = new Set<string>()
|
|
161
|
+
for (const token of tokens) {
|
|
162
|
+
if (token.length < 3) continue
|
|
163
|
+
if (/^\d+$/.test(token)) continue
|
|
164
|
+
if (MEMORY_SUBJECT_STOP_WORDS.has(token)) continue
|
|
165
|
+
if (MEMORY_VOLATILE_STOP_WORDS.has(token)) continue
|
|
166
|
+
if (seen.has(token)) continue
|
|
167
|
+
seen.add(token)
|
|
168
|
+
out.push(token)
|
|
169
|
+
}
|
|
170
|
+
return out
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function isMeaningfulMemoryTitle(title: string): boolean {
|
|
174
|
+
const normalized = stripGeneratedMemoryPrefix(title).trim()
|
|
175
|
+
if (!normalized) return false
|
|
176
|
+
if (normalizeMemoryText(normalized) === 'untitled') return false
|
|
177
|
+
return tokenizeMemorySubject(normalized).length > 0
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function buildMemorySubjectKey(title: string, content: string): string | null {
|
|
181
|
+
const titleTokens = tokenizeMemorySubject(stripGeneratedMemoryPrefix(title))
|
|
182
|
+
const contentTokens = tokenizeMemorySubject(content)
|
|
183
|
+
const preferred = [...titleTokens, ...contentTokens]
|
|
184
|
+
const out: string[] = []
|
|
185
|
+
const seen = new Set<string>()
|
|
186
|
+
for (const token of preferred) {
|
|
187
|
+
if (seen.has(token)) continue
|
|
188
|
+
seen.add(token)
|
|
189
|
+
out.push(token)
|
|
190
|
+
if (out.length >= 4) break
|
|
191
|
+
}
|
|
192
|
+
return out.length >= 2 ? out.join('|') : null
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function mergeMemoryMetadata(
|
|
196
|
+
base: Record<string, unknown> | undefined,
|
|
197
|
+
patch: Record<string, unknown>,
|
|
198
|
+
): Record<string, unknown> {
|
|
199
|
+
const next = { ...(base || {}), ...patch }
|
|
200
|
+
if (!next.tier || next.tier === 'durable') delete next.tier
|
|
201
|
+
if (!next.origin) delete next.origin
|
|
202
|
+
if (!next.subjectKey) delete next.subjectKey
|
|
203
|
+
if (!next.supersededBy) delete next.supersededBy
|
|
204
|
+
if (!next.supersededReason) delete next.supersededReason
|
|
205
|
+
if (!next.supersededAt) delete next.supersededAt
|
|
206
|
+
return next
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function selectCanonicalMemoryCandidates(args: {
|
|
210
|
+
memDb: ReturnType<typeof getMemoryDb>
|
|
211
|
+
agentId: string | null
|
|
212
|
+
title: string
|
|
213
|
+
content: string
|
|
214
|
+
canReadMemory: (entry: MemoryEntry) => boolean
|
|
215
|
+
canMutateMemory: (entry: MemoryEntry) => boolean
|
|
216
|
+
scopeFilter: MemoryScopeFilter
|
|
217
|
+
}): CanonicalMemoryCandidate[] {
|
|
218
|
+
if (!args.agentId) return []
|
|
219
|
+
const desiredTitle = stripGeneratedMemoryPrefix(args.title)
|
|
220
|
+
const desiredText = [isMeaningfulMemoryTitle(desiredTitle) ? desiredTitle : '', args.content]
|
|
221
|
+
.filter(Boolean)
|
|
222
|
+
.join('\n')
|
|
223
|
+
.trim()
|
|
224
|
+
const desiredTokens = tokenizeMemorySubject(desiredText)
|
|
225
|
+
if (desiredTokens.length < 2) return []
|
|
226
|
+
const desiredTitleNorm = normalizeMemoryText(desiredTitle)
|
|
227
|
+
const desiredSubjectKey = buildMemorySubjectKey(desiredTitle, args.content)
|
|
228
|
+
const candidateQuery = [desiredTitle, args.content].filter(Boolean).join(' ').slice(0, 400)
|
|
229
|
+
const merged = new Map<string, MemoryEntry>()
|
|
230
|
+
const sources = [
|
|
231
|
+
...(candidateQuery
|
|
232
|
+
? args.memDb.search(candidateQuery, args.agentId, { scope: args.scopeFilter, rerankMode: 'balanced' })
|
|
233
|
+
: []),
|
|
234
|
+
...args.memDb.list(args.agentId, 80),
|
|
235
|
+
]
|
|
236
|
+
for (const entry of sources) {
|
|
237
|
+
if (merged.has(entry.id)) continue
|
|
238
|
+
if (!args.canReadMemory(entry) || !args.canMutateMemory(entry)) continue
|
|
239
|
+
if (getMemoryTier(entry) !== 'durable') continue
|
|
240
|
+
if (shouldHideFromDurableRecall(entry)) continue
|
|
241
|
+
merged.set(entry.id, entry)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const matches: CanonicalMemoryCandidate[] = []
|
|
245
|
+
for (const entry of merged.values()) {
|
|
246
|
+
const entryTitle = stripGeneratedMemoryPrefix(entry.title || '')
|
|
247
|
+
const entryTitleNorm = normalizeMemoryText(entryTitle)
|
|
248
|
+
const entryTokens = tokenizeMemorySubject([entryTitle, entry.content || ''].join('\n'))
|
|
249
|
+
if (!entryTokens.length) continue
|
|
250
|
+
const shared = desiredTokens.filter((token) => entryTokens.includes(token)).length
|
|
251
|
+
const overlap = shared / Math.max(1, Math.min(desiredTokens.length, entryTokens.length))
|
|
252
|
+
const entrySubjectKey = typeof entry.metadata?.subjectKey === 'string' && entry.metadata.subjectKey.trim()
|
|
253
|
+
? entry.metadata.subjectKey.trim()
|
|
254
|
+
: buildMemorySubjectKey(entryTitle, entry.content || '')
|
|
255
|
+
const titleExact = isMeaningfulMemoryTitle(desiredTitle) && desiredTitleNorm === entryTitleNorm
|
|
256
|
+
const subjectKeyMatch = Boolean(desiredSubjectKey && entrySubjectKey && desiredSubjectKey === entrySubjectKey)
|
|
257
|
+
const score = overlap
|
|
258
|
+
+ (shared * 0.12)
|
|
259
|
+
+ (titleExact ? 1.5 : 0)
|
|
260
|
+
+ (subjectKeyMatch ? 0.9 : 0)
|
|
261
|
+
+ (entry.category.startsWith('projects/') || entry.category.startsWith('knowledge/') ? 0.08 : 0)
|
|
262
|
+
const confident = titleExact
|
|
263
|
+
|| subjectKeyMatch
|
|
264
|
+
|| (shared >= 3 && overlap >= 0.5)
|
|
265
|
+
|| (shared >= 2 && overlap >= 0.72)
|
|
266
|
+
if (!confident) continue
|
|
267
|
+
matches.push({ entry, score, sharedTokens: shared, overlap })
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
matches.sort((left, right) => {
|
|
271
|
+
if (right.score !== left.score) return right.score - left.score
|
|
272
|
+
return (right.entry.updatedAt || 0) - (left.entry.updatedAt || 0)
|
|
273
|
+
})
|
|
274
|
+
return matches
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function shouldAutoCaptureAutonomousTurn(ctx: {
|
|
278
|
+
source: string
|
|
279
|
+
response: string
|
|
280
|
+
toolEvents?: Array<{ name?: string }>
|
|
281
|
+
}): boolean {
|
|
282
|
+
if (!ctx.source || ctx.source === 'chat' || ctx.source === 'connector') return false
|
|
283
|
+
const response = (ctx.response || '').trim()
|
|
284
|
+
if (response.length < 60) return false
|
|
285
|
+
if (/^(?:HEARTBEAT_OK|NO_MESSAGE)\b/i.test(response)) return false
|
|
286
|
+
const toolEvents = Array.isArray(ctx.toolEvents) ? ctx.toolEvents : []
|
|
287
|
+
return toolEvents.some((event) => typeof event?.name === 'string' && event.name.trim().length > 0)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export async function executeMemoryAction(input: unknown, ctx: MemoryActionContext | null | undefined) {
|
|
291
|
+
const normalized = normalizeStructuredMemoryArgs(
|
|
292
|
+
normalizeToolInputArgs((input ?? {}) as Record<string, unknown>),
|
|
293
|
+
)
|
|
294
|
+
const n = normalized as Record<string, unknown>
|
|
28
295
|
const {
|
|
29
296
|
action, key, value, query, scope, rerank,
|
|
30
297
|
scopeSessionId, projectRoot, filePaths, references, project,
|
|
31
|
-
linkedMemoryIds,
|
|
32
|
-
|
|
298
|
+
linkedMemoryIds, targetIds,
|
|
299
|
+
pinned, sharedWith,
|
|
33
300
|
} = n
|
|
34
|
-
const
|
|
301
|
+
const actionText = typeof action === 'string' ? action.trim() : ''
|
|
302
|
+
const keyText = typeof key === 'string' ? key.trim() : ''
|
|
303
|
+
const hasValueText = typeof value === 'string'
|
|
304
|
+
const valueText = hasValueText ? value : ''
|
|
305
|
+
const queryText = typeof query === 'string' ? query : ''
|
|
306
|
+
const requestedCategory = typeof n.category === 'string' && n.category.trim()
|
|
307
|
+
? n.category.trim()
|
|
308
|
+
: undefined
|
|
309
|
+
const normalizedLinkedMemoryIds = Array.isArray(linkedMemoryIds)
|
|
310
|
+
? linkedMemoryIds.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0)
|
|
311
|
+
: undefined
|
|
312
|
+
const resolvedAction = actionText || 'list'
|
|
313
|
+
const explicitMemoryId = typeof n.id === 'string' && n.id.trim()
|
|
314
|
+
? n.id.trim()
|
|
315
|
+
: ''
|
|
316
|
+
const memoryId = explicitMemoryId
|
|
317
|
+
? explicitMemoryId
|
|
318
|
+
: keyText
|
|
319
|
+
? keyText
|
|
320
|
+
: ''
|
|
321
|
+
const memoryTitle = typeof n.title === 'string' && n.title.trim()
|
|
322
|
+
? n.title.trim()
|
|
323
|
+
: keyText
|
|
324
|
+
? keyText
|
|
325
|
+
: 'Untitled'
|
|
35
326
|
const imagePath = typeof n.imagePath === 'string' ? n.imagePath : undefined
|
|
36
327
|
|
|
37
328
|
const memDb = getMemoryDb()
|
|
@@ -41,13 +332,13 @@ async function executeMemoryAction(input: any, ctx: any) {
|
|
|
41
332
|
: typeof ctx?.id === 'string'
|
|
42
333
|
? ctx.id
|
|
43
334
|
: null
|
|
44
|
-
const currentSession =
|
|
335
|
+
const currentSession = isSessionContext(ctx) ? ctx : null
|
|
45
336
|
const configuredScope = typeof ctx?.memoryScopeMode === 'string' ? ctx.memoryScopeMode : 'auto'
|
|
46
337
|
const rawScope = typeof scope === 'string' ? scope : configuredScope
|
|
47
338
|
const scopeMode = normalizeMemoryScopeMode(rawScope === 'shared' ? 'global' : rawScope)
|
|
48
339
|
const rerankMode = rerank === 'semantic' || rerank === 'lexical' ? rerank : 'balanced'
|
|
49
340
|
|
|
50
|
-
const scopeFilter = {
|
|
341
|
+
const scopeFilter: MemoryScopeFilter = {
|
|
51
342
|
mode: scopeMode,
|
|
52
343
|
agentId: currentAgentId,
|
|
53
344
|
sessionId: (typeof scopeSessionId === 'string' && scopeSessionId.trim()) ? scopeSessionId.trim() : currentSessionId,
|
|
@@ -64,58 +355,130 @@ async function executeMemoryAction(input: any, ctx: any) {
|
|
|
64
355
|
|
|
65
356
|
const limits = getMemoryLookupLimits(loadSettings())
|
|
66
357
|
const maxPerLookup = limits.maxPerLookup
|
|
358
|
+
const searchSources = normalizeMemorySearchSources(n.sources)
|
|
359
|
+
const inputMetadata = n.metadata && typeof n.metadata === 'object' && !Array.isArray(n.metadata)
|
|
360
|
+
? { ...(n.metadata as Record<string, unknown>) }
|
|
361
|
+
: {}
|
|
362
|
+
if (scopeMode === 'project' && scopeFilter.projectRoot && !inputMetadata.projectRoot) {
|
|
363
|
+
inputMetadata.projectRoot = scopeFilter.projectRoot
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const buildCanonicalMetadata = (title: string, content: string, extra?: Record<string, unknown>) => {
|
|
367
|
+
const subjectKey = buildMemorySubjectKey(title, content)
|
|
368
|
+
return mergeMemoryMetadata(inputMetadata, {
|
|
369
|
+
...extra,
|
|
370
|
+
subjectKey: subjectKey || undefined,
|
|
371
|
+
tier: extra?.tier,
|
|
372
|
+
supersededBy: extra?.supersededBy,
|
|
373
|
+
supersededReason: extra?.supersededReason,
|
|
374
|
+
supersededAt: extra?.supersededAt,
|
|
375
|
+
})
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const findRelatedCanonicalCandidates = (title: string, content: string) => selectCanonicalMemoryCandidates({
|
|
379
|
+
memDb,
|
|
380
|
+
agentId: currentAgentId,
|
|
381
|
+
title,
|
|
382
|
+
content,
|
|
383
|
+
canReadMemory,
|
|
384
|
+
canMutateMemory,
|
|
385
|
+
scopeFilter,
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
const supersedeCompetingMemories = (targetId: string, title: string, content: string, related: CanonicalMemoryCandidate[]) => {
|
|
389
|
+
const subjectKey = buildMemorySubjectKey(title, content)
|
|
390
|
+
const seen = new Set<string>()
|
|
391
|
+
for (const candidate of related) {
|
|
392
|
+
const entry = candidate.entry
|
|
393
|
+
if (entry.id === targetId || seen.has(entry.id)) continue
|
|
394
|
+
seen.add(entry.id)
|
|
395
|
+
const nextMetadata = mergeMemoryMetadata(entry.metadata, {
|
|
396
|
+
subjectKey: subjectKey || undefined,
|
|
397
|
+
supersededBy: targetId,
|
|
398
|
+
supersededReason: 'canonical-upsert',
|
|
399
|
+
supersededAt: Date.now(),
|
|
400
|
+
tier: 'working',
|
|
401
|
+
})
|
|
402
|
+
memDb.update(entry.id, {
|
|
403
|
+
metadata: nextMetadata,
|
|
404
|
+
})
|
|
405
|
+
}
|
|
406
|
+
}
|
|
67
407
|
|
|
68
|
-
if ((
|
|
408
|
+
if ((resolvedAction === 'search' || resolvedAction === 'list') && currentSession && (searchSources.has('archive') || searchSources.has('all'))) {
|
|
69
409
|
try { syncSessionArchiveMemory(currentSession) } catch { /* archive sync is best-effort */ }
|
|
70
410
|
}
|
|
71
411
|
|
|
72
|
-
const formatEntry = (m:
|
|
412
|
+
const formatEntry = (m: MemoryEntry) => {
|
|
73
413
|
let line = `[${m.id}] (${m.agentId ? `agent:${m.agentId}` : 'shared'}) ${m.category}/${m.title}: ${m.content}`
|
|
74
414
|
if (m.reinforcementCount) line += ` (reinforced ×${m.reinforcementCount})`
|
|
75
415
|
if (m.references?.length) {
|
|
76
|
-
line += `\n refs: ${m.references.map((r:
|
|
416
|
+
line += `\n refs: ${m.references.map((r: MemoryReference) => `${r.type}:${r.path || r.title || r.type}`).join(', ')}`
|
|
77
417
|
}
|
|
78
418
|
if (m.imagePath) line += `\n image: ${m.imagePath}`
|
|
79
419
|
if (m.linkedMemoryIds?.length) line += `\n linked: ${m.linkedMemoryIds.join(', ')}`
|
|
80
420
|
return line
|
|
81
421
|
}
|
|
82
422
|
|
|
83
|
-
if (
|
|
84
|
-
|
|
423
|
+
if (resolvedAction === 'store') {
|
|
424
|
+
const fallbackValueText = latestUserFactFromSession(currentSession)
|
|
425
|
+
const storedValueText = hasValueText && valueText.trim()
|
|
426
|
+
? valueText
|
|
427
|
+
: fallbackValueText
|
|
428
|
+
if (!storedValueText.trim()) {
|
|
429
|
+
return 'Memory store requires a non-empty value.'
|
|
430
|
+
}
|
|
431
|
+
let storedImage: MemoryImage | null = null
|
|
85
432
|
if (imagePath && fs.existsSync(imagePath)) {
|
|
86
433
|
storedImage = await storeMemoryImageAsset(imagePath, genId(6))
|
|
87
434
|
}
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
435
|
+
const normalizedCategory = normalizeMemoryCategory(requestedCategory || 'note', memoryTitle, storedValueText)
|
|
436
|
+
const related = findRelatedCanonicalCandidates(memoryTitle, storedValueText)
|
|
437
|
+
const canonicalTarget = related[0]?.entry || null
|
|
438
|
+
const canonicalMetadata = buildCanonicalMetadata(memoryTitle, storedValueText)
|
|
439
|
+
if (canonicalTarget) {
|
|
440
|
+
const updated = memDb.update(canonicalTarget.id, {
|
|
441
|
+
title: memoryTitle,
|
|
442
|
+
content: storedValueText,
|
|
443
|
+
category: normalizedCategory,
|
|
444
|
+
metadata: mergeMemoryMetadata(canonicalTarget.metadata, canonicalMetadata),
|
|
445
|
+
references: Array.isArray(references) ? references as MemoryReference[] : canonicalTarget.references,
|
|
446
|
+
filePaths: Array.isArray(filePaths) ? filePaths as FileReference[] : canonicalTarget.filePaths,
|
|
447
|
+
imagePath: storedImage?.path || canonicalTarget.imagePath,
|
|
448
|
+
linkedMemoryIds: normalizedLinkedMemoryIds || canonicalTarget.linkedMemoryIds,
|
|
449
|
+
pinned: typeof pinned === 'boolean' ? pinned : canonicalTarget.pinned,
|
|
450
|
+
sharedWith: Array.isArray(sharedWith) ? sharedWith : canonicalTarget.sharedWith,
|
|
451
|
+
})
|
|
452
|
+
if (updated) {
|
|
453
|
+
supersedeCompetingMemories(updated.id, memoryTitle, storedValueText, related)
|
|
454
|
+
return `Stored memory "${updated.title}" (id: ${updated.id}) in ${normalizedCategory} by updating the canonical entry. No further memory lookup is needed unless the user asked you to verify.`
|
|
455
|
+
}
|
|
93
456
|
}
|
|
94
457
|
const entry = memDb.add({
|
|
95
458
|
agentId: scopeMode === 'global' ? null : currentAgentId,
|
|
96
459
|
sessionId: ctx?.sessionId || null,
|
|
97
|
-
category:
|
|
98
|
-
title:
|
|
99
|
-
content:
|
|
100
|
-
metadata,
|
|
101
|
-
references: Array.isArray(references) ? references : [],
|
|
102
|
-
filePaths: filePaths as
|
|
460
|
+
category: normalizedCategory,
|
|
461
|
+
title: memoryTitle,
|
|
462
|
+
content: storedValueText,
|
|
463
|
+
metadata: canonicalMetadata,
|
|
464
|
+
references: Array.isArray(references) ? references as MemoryReference[] : [],
|
|
465
|
+
filePaths: Array.isArray(filePaths) ? filePaths as FileReference[] : undefined,
|
|
103
466
|
imagePath: storedImage?.path || undefined,
|
|
104
|
-
linkedMemoryIds,
|
|
467
|
+
linkedMemoryIds: normalizedLinkedMemoryIds,
|
|
105
468
|
pinned: pinned === true,
|
|
106
469
|
sharedWith: Array.isArray(sharedWith) ? sharedWith : undefined,
|
|
107
470
|
})
|
|
108
|
-
return `Stored memory "${
|
|
471
|
+
return `Stored memory "${entry.title}" (id: ${entry.id}) in ${normalizedCategory}. No further memory lookup is needed unless the user asked you to verify.`
|
|
109
472
|
}
|
|
110
473
|
|
|
111
|
-
if (
|
|
112
|
-
const found = memDb.get(
|
|
113
|
-
if (!found || !canReadMemory(found)) return `Memory not found or access denied: ${
|
|
474
|
+
if (resolvedAction === 'get') {
|
|
475
|
+
const found = memDb.get(memoryId)
|
|
476
|
+
if (!found || !canReadMemory(found)) return `Memory not found or access denied: ${memoryId}`
|
|
114
477
|
return formatEntry(found)
|
|
115
478
|
}
|
|
116
479
|
|
|
117
|
-
if (
|
|
118
|
-
const queries =
|
|
480
|
+
if (resolvedAction === 'search') {
|
|
481
|
+
const queries = queryText ? await expandQuery(queryText) : [keyText]
|
|
119
482
|
const allResults: MemoryEntry[] = []
|
|
120
483
|
const seenIds = new Set<string>()
|
|
121
484
|
for (const q of queries) {
|
|
@@ -126,23 +489,103 @@ async function executeMemoryAction(input: any, ctx: any) {
|
|
|
126
489
|
}
|
|
127
490
|
}
|
|
128
491
|
}
|
|
129
|
-
|
|
130
|
-
|
|
492
|
+
const scopedResults = filterResultsBySources(allResults, searchSources)
|
|
493
|
+
const visibleResults = scopedResults.length ? scopedResults : allResults
|
|
494
|
+
if (!visibleResults.length) return 'No memories found.'
|
|
495
|
+
return visibleResults.slice(0, maxPerLookup).map(formatEntry).join('\n')
|
|
131
496
|
}
|
|
132
497
|
|
|
133
|
-
if (
|
|
498
|
+
if (resolvedAction === 'list') {
|
|
134
499
|
const results = filterScope(memDb.list(undefined, maxPerLookup))
|
|
135
|
-
|
|
500
|
+
const scopedResults = filterResultsBySources(results, searchSources)
|
|
501
|
+
const visibleResults = scopedResults.length ? scopedResults : results
|
|
502
|
+
return visibleResults.length ? visibleResults.map(formatEntry).join('\n') : 'No memories stored yet.'
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (resolvedAction === 'delete') {
|
|
506
|
+
const found = memDb.get(memoryId)
|
|
507
|
+
if (!found || !canMutateMemory(found)) return 'Memory not found or access denied.'
|
|
508
|
+
memDb.delete(memoryId)
|
|
509
|
+
return `Deleted memory "${memoryId}"`
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (resolvedAction === 'update') {
|
|
513
|
+
const exact = memoryId ? memDb.get(memoryId) : null
|
|
514
|
+
const nextTitleSeed = typeof n.title === 'string' && n.title.trim()
|
|
515
|
+
? n.title.trim()
|
|
516
|
+
: keyText
|
|
517
|
+
? keyText
|
|
518
|
+
: exact?.title || memoryTitle
|
|
519
|
+
const nextContentSeed = hasValueText && valueText.trim()
|
|
520
|
+
? valueText
|
|
521
|
+
: queryText.trim()
|
|
522
|
+
? queryText.trim()
|
|
523
|
+
: exact?.content || ''
|
|
524
|
+
const related = findRelatedCanonicalCandidates(nextTitleSeed, nextContentSeed)
|
|
525
|
+
const found = exact && canMutateMemory(exact)
|
|
526
|
+
? exact
|
|
527
|
+
: related[0]?.entry || null
|
|
528
|
+
if (!found) {
|
|
529
|
+
if (explicitMemoryId) return 'Memory not found or access denied.'
|
|
530
|
+
if (!nextContentSeed.trim()) return 'Memory update requires id, key, title, or query.'
|
|
531
|
+
const normalizedCategory = normalizeMemoryCategory(requestedCategory || 'note', nextTitleSeed, nextContentSeed)
|
|
532
|
+
const created = memDb.add({
|
|
533
|
+
agentId: scopeMode === 'global' ? null : currentAgentId,
|
|
534
|
+
sessionId: ctx?.sessionId || null,
|
|
535
|
+
category: normalizedCategory,
|
|
536
|
+
title: nextTitleSeed,
|
|
537
|
+
content: nextContentSeed,
|
|
538
|
+
metadata: buildCanonicalMetadata(nextTitleSeed, nextContentSeed),
|
|
539
|
+
references: Array.isArray(references) ? references as MemoryReference[] : [],
|
|
540
|
+
filePaths: Array.isArray(filePaths) ? filePaths as FileReference[] : undefined,
|
|
541
|
+
linkedMemoryIds: normalizedLinkedMemoryIds,
|
|
542
|
+
pinned: pinned === true,
|
|
543
|
+
sharedWith: Array.isArray(sharedWith) ? sharedWith : undefined,
|
|
544
|
+
})
|
|
545
|
+
return `Updated memory "${created.title}" (id: ${created.id}) by creating a new canonical entry. No further memory lookup is needed unless the user asked you to verify.`
|
|
546
|
+
}
|
|
547
|
+
const nextTitle = typeof n.title === 'string' && n.title.trim() ? n.title.trim() : found.title
|
|
548
|
+
const nextContent = hasValueText && valueText.trim() ? valueText : found.content
|
|
549
|
+
const updates: Partial<MemoryEntry> = {
|
|
550
|
+
title: nextTitle,
|
|
551
|
+
content: nextContent,
|
|
552
|
+
category: requestedCategory
|
|
553
|
+
? normalizeMemoryCategory(requestedCategory, nextTitle, nextContent)
|
|
554
|
+
: found.category,
|
|
555
|
+
metadata: mergeMemoryMetadata(found.metadata, buildCanonicalMetadata(nextTitle, nextContent)),
|
|
556
|
+
}
|
|
557
|
+
if (normalizedLinkedMemoryIds) updates.linkedMemoryIds = normalizedLinkedMemoryIds
|
|
558
|
+
if (Array.isArray(sharedWith)) updates.sharedWith = sharedWith
|
|
559
|
+
if (typeof pinned === 'boolean') updates.pinned = pinned
|
|
560
|
+
if (Array.isArray(references)) updates.references = references as MemoryReference[]
|
|
561
|
+
if (Array.isArray(filePaths)) updates.filePaths = filePaths as FileReference[]
|
|
562
|
+
const updated = memDb.update(found.id, updates)
|
|
563
|
+
if (!updated) return `Memory not found: ${memoryId}`
|
|
564
|
+
supersedeCompetingMemories(updated.id, nextTitle, nextContent, related)
|
|
565
|
+
return `Updated memory "${updated.title}" (id: ${updated.id}). No further memory lookup is needed unless the user asked you to verify.`
|
|
136
566
|
}
|
|
137
567
|
|
|
138
|
-
if (
|
|
139
|
-
|
|
568
|
+
if (resolvedAction === 'link' || resolvedAction === 'unlink') {
|
|
569
|
+
if (!memoryId) return `Memory ${resolvedAction} requires id or key.`
|
|
570
|
+
const found = memDb.get(memoryId)
|
|
140
571
|
if (!found || !canMutateMemory(found)) return 'Memory not found or access denied.'
|
|
141
|
-
|
|
142
|
-
|
|
572
|
+
const ids = Array.isArray(targetIds)
|
|
573
|
+
? targetIds.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0)
|
|
574
|
+
: []
|
|
575
|
+
if (ids.length === 0) return `${resolvedAction} requires targetIds.`
|
|
576
|
+
const updated = resolvedAction === 'link'
|
|
577
|
+
? memDb.link(memoryId, ids, true)
|
|
578
|
+
: memDb.unlink(memoryId, ids, true)
|
|
579
|
+
if (!updated) return `Memory not found: ${memoryId}`
|
|
580
|
+
return `${resolvedAction === 'link' ? 'Linked' : 'Unlinked'} ${ids.length} memories for "${updated.title}" (id: ${updated.id})`
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (resolvedAction === 'doctor') {
|
|
584
|
+
const visible = filterScope(memDb.list(undefined, maxPerLookup))
|
|
585
|
+
return buildMemoryDoctorReport(visible, currentAgentId)
|
|
143
586
|
}
|
|
144
587
|
|
|
145
|
-
return `Unknown action "${
|
|
588
|
+
return `Unknown action "${resolvedAction}".`
|
|
146
589
|
}
|
|
147
590
|
|
|
148
591
|
/**
|
|
@@ -155,8 +598,7 @@ const MemoryPlugin: Plugin = {
|
|
|
155
598
|
getAgentContext: async (ctx) => {
|
|
156
599
|
const agentId = ctx.session.agentId
|
|
157
600
|
if (!agentId) return null
|
|
158
|
-
|
|
159
|
-
try { syncSessionArchiveMemory(ctx.session) } catch { /* archive sync is best-effort */ }
|
|
601
|
+
if (!shouldInjectMemoryContext(ctx.message || '')) return null
|
|
160
602
|
|
|
161
603
|
const memDb = getMemoryDb()
|
|
162
604
|
const memoryQuerySeed = [
|
|
@@ -178,30 +620,37 @@ const MemoryPlugin: Plugin = {
|
|
|
178
620
|
|
|
179
621
|
const pinned = memDb.listPinned(agentId, 5)
|
|
180
622
|
const pinnedLines = pinned
|
|
181
|
-
.filter((m) => {
|
|
623
|
+
.filter((m) => {
|
|
624
|
+
if (!m?.id || seen.has(m.id)) return false
|
|
625
|
+
if (shouldHideFromDurableRecall(m)) return false
|
|
626
|
+
seen.add(m.id)
|
|
627
|
+
return true
|
|
628
|
+
})
|
|
182
629
|
.map(formatMemoryLine)
|
|
183
630
|
|
|
184
631
|
const relevantSlice = Math.max(2, 6 - pinnedLines.length)
|
|
185
632
|
const relevantLookup = memDb.searchWithLinked(memoryQuerySeed, agentId, 1, 10, 14)
|
|
186
|
-
const relevant = relevantLookup.entries.slice(0, relevantSlice)
|
|
187
633
|
const recent = memDb.list(agentId, 12).slice(0, 6)
|
|
188
|
-
const relevantByTier = partitionMemoriesByTier(
|
|
634
|
+
const relevantByTier = partitionMemoriesByTier(relevantLookup.entries)
|
|
189
635
|
const recentByTier = partitionMemoriesByTier(recent)
|
|
190
636
|
|
|
191
637
|
const relevantLines = relevantByTier.durable
|
|
192
|
-
.filter((m) => {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
638
|
+
.filter((m) => {
|
|
639
|
+
if (!m?.id || seen.has(m.id)) return false
|
|
640
|
+
if (shouldHideFromDurableRecall(m)) return false
|
|
641
|
+
seen.add(m.id)
|
|
642
|
+
return true
|
|
643
|
+
})
|
|
644
|
+
.slice(0, relevantSlice)
|
|
197
645
|
.map(formatMemoryLine)
|
|
198
646
|
|
|
199
647
|
const recentLines = recentByTier.durable
|
|
200
|
-
.filter((m) => {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
648
|
+
.filter((m) => {
|
|
649
|
+
if (!m?.id || seen.has(m.id)) return false
|
|
650
|
+
if (shouldHideFromDurableRecall(m)) return false
|
|
651
|
+
seen.add(m.id)
|
|
652
|
+
return true
|
|
653
|
+
})
|
|
205
654
|
.map(formatMemoryLine)
|
|
206
655
|
|
|
207
656
|
const parts: string[] = []
|
|
@@ -211,21 +660,15 @@ const MemoryPlugin: Plugin = {
|
|
|
211
660
|
if (relevantLines.length) {
|
|
212
661
|
parts.push(['## Relevant Memory Hits', 'These memories were retrieved by relevance for the current objective.', ...relevantLines].join('\n'))
|
|
213
662
|
}
|
|
214
|
-
if (archiveLines.length) {
|
|
215
|
-
parts.push(['## Session Archive Hits', 'Past conversation snapshots that may restore context from older chats.', ...archiveLines].join('\n'))
|
|
216
|
-
}
|
|
217
663
|
if (recentLines.length) {
|
|
218
664
|
parts.push(['## Recent Memory Notes', 'Recent durable notes that may still apply.', ...recentLines].join('\n'))
|
|
219
665
|
}
|
|
220
|
-
if (recentArchiveLines.length) {
|
|
221
|
-
parts.push(['## Recent Session Archives', 'Recently synced conversation archives you can search instead of relying on stale live context.', ...recentArchiveLines].join('\n'))
|
|
222
|
-
}
|
|
223
666
|
|
|
224
667
|
// Memory Policy
|
|
225
668
|
parts.push([
|
|
226
669
|
'## My Memory',
|
|
227
|
-
'I have long-term memory that persists across conversations. I use it
|
|
228
|
-
'Memory tiers: working memory is short-lived, durable memory stores stable facts and decisions, and session archives
|
|
670
|
+
'I have long-term memory that persists across conversations. I use it when the user asks me to remember something or when I need to recall past conversations.',
|
|
671
|
+
'Memory tiers: working memory is short-lived, durable memory stores stable facts and decisions, and session archives are available separately when explicitly needed.',
|
|
229
672
|
'',
|
|
230
673
|
'**Things worth remembering:**',
|
|
231
674
|
'- What the user likes, dislikes, or has corrected me on',
|
|
@@ -243,8 +686,8 @@ const MemoryPlugin: Plugin = {
|
|
|
243
686
|
'',
|
|
244
687
|
'**Good habits:**',
|
|
245
688
|
'- Give memories clear titles ("User prefers dark mode" not "Note 1")',
|
|
246
|
-
'- Use categories:
|
|
247
|
-
'-
|
|
689
|
+
'- Use categories: identity/preferences, identity/relationships, projects/decisions, projects/learnings, operations/environment, knowledge/facts',
|
|
690
|
+
'- Prefer durable memories first; only inspect session archives when transcript history is specifically needed',
|
|
248
691
|
'- Check what I already know before storing something new',
|
|
249
692
|
'- When I learn something that corrects old knowledge, update or remove the old memory',
|
|
250
693
|
].join('\n'))
|
|
@@ -281,15 +724,14 @@ const MemoryPlugin: Plugin = {
|
|
|
281
724
|
} catch { /* breadcrumbs are best-effort */ }
|
|
282
725
|
},
|
|
283
726
|
afterChatTurn: (ctx) => {
|
|
284
|
-
if (ctx.internal) return
|
|
285
|
-
if (ctx.source !== 'chat' && ctx.source !== 'connector') return
|
|
286
727
|
const agentId = ctx.session.agentId
|
|
287
728
|
if (!agentId) return
|
|
288
729
|
const msg = (ctx.message || '').trim()
|
|
289
730
|
const resp = (ctx.response || '').trim()
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
731
|
+
const shouldCapture = ctx.internal
|
|
732
|
+
? shouldAutoCaptureAutonomousTurn(ctx)
|
|
733
|
+
: ((ctx.source === 'chat' || ctx.source === 'connector') && shouldAutoCaptureMemoryTurn(msg, resp))
|
|
734
|
+
if (!shouldCapture) return
|
|
293
735
|
const now = Date.now()
|
|
294
736
|
const last = typeof ctx.session.lastAutoMemoryAt === 'number' ? ctx.session.lastAutoMemoryAt : 0
|
|
295
737
|
if (last > 0 && now - last < 5 * 60 * 1000) return
|
|
@@ -297,30 +739,55 @@ const MemoryPlugin: Plugin = {
|
|
|
297
739
|
const memDb = getMemoryDb()
|
|
298
740
|
const compactMessage = msg.replace(/\s+/g, ' ').slice(0, 220)
|
|
299
741
|
const compactResponse = resp.replace(/\s+/g, ' ').slice(0, 700)
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
742
|
+
const compactToolNames = Array.isArray(ctx.toolEvents)
|
|
743
|
+
? ctx.toolEvents
|
|
744
|
+
.map((event) => String(event?.name || '').trim())
|
|
745
|
+
.filter(Boolean)
|
|
746
|
+
.slice(0, 8)
|
|
747
|
+
: []
|
|
748
|
+
const autoTitleSeed = compactMessage || compactResponse
|
|
749
|
+
const autoTitle = `[auto] ${autoTitleSeed.slice(0, 90)}`
|
|
750
|
+
const content = [
|
|
751
|
+
`source: ${ctx.source}`,
|
|
752
|
+
compactToolNames.length > 0 ? `tools: ${compactToolNames.join(', ')}` : '',
|
|
753
|
+
compactMessage ? `user_request: ${compactMessage}` : '',
|
|
754
|
+
`assistant_outcome: ${compactResponse}`,
|
|
755
|
+
].filter(Boolean).join('\n')
|
|
756
|
+
memDb.add({
|
|
757
|
+
agentId,
|
|
758
|
+
sessionId: ctx.session.id,
|
|
759
|
+
category: normalizeMemoryCategory('execution', autoTitle, content),
|
|
760
|
+
title: autoTitle,
|
|
761
|
+
content,
|
|
762
|
+
})
|
|
303
763
|
ctx.session.lastAutoMemoryAt = now
|
|
304
764
|
} catch { /* auto-memory is best-effort */ }
|
|
305
765
|
},
|
|
306
766
|
getCapabilityDescription: () => 'I have long-term memory (`memory_tool`) — I can remember things across conversations and recall them when needed.',
|
|
307
767
|
getOperatingGuidance: () => [
|
|
308
|
-
'Memory:
|
|
768
|
+
'Memory: use memory_tool only when recalling past conversations or when explicitly asked to remember. For info already in the current conversation, respond directly without tool calls.',
|
|
769
|
+
'When the user directly says to remember, store, or correct a fact, do one memory_tool store/update call immediately. Treat the newest direct user statement as authoritative.',
|
|
770
|
+
'memory_tool store/update now merges canonical memories and retires superseded variants. After a successful store/update, do not keep re-searching unless the user explicitly asked you to verify.',
|
|
771
|
+
'By default, memory searches focus on durable memories. Only include archives or working execution notes when you explicitly need transcript or run-history context.',
|
|
309
772
|
'For open goals, form a hypothesis and execute — do not keep re-asking broad questions.',
|
|
310
773
|
],
|
|
311
774
|
} as PluginHooks,
|
|
312
775
|
tools: [
|
|
313
776
|
{
|
|
314
777
|
name: 'memory_tool',
|
|
315
|
-
description: 'Advanced long-term memory system.
|
|
778
|
+
description: 'Advanced long-term memory system. Store and update canonical durable facts across conversations; store/update will merge matching memories and retire superseded variants. Search defaults to durable memories unless sources explicitly include archive or working.',
|
|
316
779
|
parameters: {
|
|
317
780
|
type: 'object',
|
|
318
781
|
properties: {
|
|
319
|
-
action: { type: 'string', enum: ['store', 'get', 'search', 'list', 'delete'] },
|
|
782
|
+
action: { type: 'string', enum: ['store', 'get', 'search', 'list', 'delete', 'update', 'link', 'unlink', 'doctor'] },
|
|
783
|
+
id: { type: 'string' },
|
|
320
784
|
key: { type: 'string' },
|
|
785
|
+
title: { type: 'string' },
|
|
321
786
|
value: { type: 'string' },
|
|
322
787
|
category: { type: 'string' },
|
|
323
788
|
query: { type: 'string' },
|
|
789
|
+
sources: { type: 'array', items: { type: 'string', enum: ['durable', 'working', 'archive', 'all'] } },
|
|
790
|
+
targetIds: { type: 'array', items: { type: 'string' } },
|
|
324
791
|
scope: { type: 'string', enum: ['auto', 'all', 'global', 'shared', 'agent', 'session', 'project'] },
|
|
325
792
|
},
|
|
326
793
|
required: ['action']
|