@swarmclawai/swarmclaw 0.7.7 → 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 -14
- 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 +23 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +46 -3
- package/src/app/api/connectors/route.ts +12 -8
- 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/projects/[id]/route.ts +6 -2
- package/src/app/api/projects/route.ts +4 -3
- 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/secrets/[id]/route.ts +1 -0
- package/src/app/api/secrets/route.ts +2 -1
- package/src/app/api/settings/route.ts +2 -0
- 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 +257 -38
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-area.tsx +36 -19
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +48 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- package/src/components/chat/delegation-banner.test.ts +14 -1
- package/src/components/chat/delegation-banner.tsx +1 -1
- 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/layout/app-layout.tsx +40 -23
- 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/projects/project-detail.tsx +217 -0
- package/src/components/projects/project-sheet.tsx +176 -4
- 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 +45 -3
- package/src/components/shared/settings/section-voice.tsx +11 -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 +289 -34
- package/src/components/tasks/task-board.tsx +410 -25
- package/src/components/tasks/task-card.tsx +66 -8
- package/src/components/tasks/task-sheet.tsx +16 -4
- 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 +33 -0
- package/src/lib/server/capability-router.ts +80 -19
- 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 +378 -73
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +461 -137
- 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 +84 -47
- 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 +247 -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 +20 -11
- 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/main-agent-loop.test.ts +260 -0
- package/src/lib/server/main-agent-loop.ts +559 -14
- 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 +3 -2
- package/src/lib/server/orchestrator.ts +2 -0
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +211 -6
- package/src/lib/server/project-context.ts +162 -0
- package/src/lib/server/project-utils.ts +150 -0
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +409 -2
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +527 -68
- 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 +83 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +366 -54
- package/src/lib/server/session-tools/context.ts +17 -3
- package/src/lib/server/session-tools/crud.ts +484 -84
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
- package/src/lib/server/session-tools/delegate.ts +102 -10
- 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/manage-tasks.test.ts +114 -0
- package/src/lib/server/session-tools/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +554 -75
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- package/src/lib/server/session-tools/platform-access.test.ts +58 -0
- package/src/lib/server/session-tools/platform.ts +60 -19
- 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 +178 -0
- package/src/lib/server/session-tools/web.ts +621 -70
- 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 +437 -2
- package/src/lib/server/stream-agent-chat.ts +957 -79
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-aliases.ts +2 -0
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- package/src/lib/server/tool-capability-policy.test.ts +24 -0
- package/src/lib/server/tool-capability-policy.ts +29 -1
- 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.test.ts +44 -0
- package/src/lib/server/tool-planning.ts +271 -0
- 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-definitions.ts +2 -1
- 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 +249 -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,16 +332,21 @@ async function executeMemoryAction(input: any, ctx: any) {
|
|
|
41
332
|
: typeof ctx?.id === 'string'
|
|
42
333
|
? ctx.id
|
|
43
334
|
: null
|
|
44
|
-
const currentSession =
|
|
45
|
-
const
|
|
335
|
+
const currentSession = isSessionContext(ctx) ? ctx : null
|
|
336
|
+
const configuredScope = typeof ctx?.memoryScopeMode === 'string' ? ctx.memoryScopeMode : 'auto'
|
|
337
|
+
const rawScope = typeof scope === 'string' ? scope : configuredScope
|
|
46
338
|
const scopeMode = normalizeMemoryScopeMode(rawScope === 'shared' ? 'global' : rawScope)
|
|
47
339
|
const rerankMode = rerank === 'semantic' || rerank === 'lexical' ? rerank : 'balanced'
|
|
48
340
|
|
|
49
|
-
const scopeFilter = {
|
|
341
|
+
const scopeFilter: MemoryScopeFilter = {
|
|
50
342
|
mode: scopeMode,
|
|
51
343
|
agentId: currentAgentId,
|
|
52
344
|
sessionId: (typeof scopeSessionId === 'string' && scopeSessionId.trim()) ? scopeSessionId.trim() : currentSessionId,
|
|
53
|
-
projectRoot: (typeof projectRoot === 'string' && projectRoot.trim())
|
|
345
|
+
projectRoot: (typeof projectRoot === 'string' && projectRoot.trim())
|
|
346
|
+
? projectRoot.trim()
|
|
347
|
+
: ((project && typeof project === 'object' && 'rootPath' in project && typeof (project as Record<string, unknown>).rootPath === 'string')
|
|
348
|
+
? (project as Record<string, unknown>).rootPath as string
|
|
349
|
+
: (typeof ctx?.projectRoot === 'string' && ctx.projectRoot.trim() ? ctx.projectRoot.trim() : null)),
|
|
54
350
|
}
|
|
55
351
|
|
|
56
352
|
const filterScope = (rows: MemoryEntry[]) => filterMemoriesByScope(rows, scopeFilter)
|
|
@@ -59,51 +355,130 @@ async function executeMemoryAction(input: any, ctx: any) {
|
|
|
59
355
|
|
|
60
356
|
const limits = getMemoryLookupLimits(loadSettings())
|
|
61
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
|
+
})
|
|
62
387
|
|
|
63
|
-
|
|
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
|
+
}
|
|
407
|
+
|
|
408
|
+
if ((resolvedAction === 'search' || resolvedAction === 'list') && currentSession && (searchSources.has('archive') || searchSources.has('all'))) {
|
|
64
409
|
try { syncSessionArchiveMemory(currentSession) } catch { /* archive sync is best-effort */ }
|
|
65
410
|
}
|
|
66
411
|
|
|
67
|
-
const formatEntry = (m:
|
|
412
|
+
const formatEntry = (m: MemoryEntry) => {
|
|
68
413
|
let line = `[${m.id}] (${m.agentId ? `agent:${m.agentId}` : 'shared'}) ${m.category}/${m.title}: ${m.content}`
|
|
69
414
|
if (m.reinforcementCount) line += ` (reinforced ×${m.reinforcementCount})`
|
|
70
415
|
if (m.references?.length) {
|
|
71
|
-
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(', ')}`
|
|
72
417
|
}
|
|
73
418
|
if (m.imagePath) line += `\n image: ${m.imagePath}`
|
|
74
419
|
if (m.linkedMemoryIds?.length) line += `\n linked: ${m.linkedMemoryIds.join(', ')}`
|
|
75
420
|
return line
|
|
76
421
|
}
|
|
77
422
|
|
|
78
|
-
if (
|
|
79
|
-
|
|
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
|
|
80
432
|
if (imagePath && fs.existsSync(imagePath)) {
|
|
81
433
|
storedImage = await storeMemoryImageAsset(imagePath, genId(6))
|
|
82
434
|
}
|
|
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
|
+
}
|
|
456
|
+
}
|
|
83
457
|
const entry = memDb.add({
|
|
84
458
|
agentId: scopeMode === 'global' ? null : currentAgentId,
|
|
85
459
|
sessionId: ctx?.sessionId || null,
|
|
86
|
-
category:
|
|
87
|
-
title:
|
|
88
|
-
content:
|
|
89
|
-
|
|
90
|
-
|
|
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,
|
|
91
466
|
imagePath: storedImage?.path || undefined,
|
|
92
|
-
linkedMemoryIds,
|
|
467
|
+
linkedMemoryIds: normalizedLinkedMemoryIds,
|
|
93
468
|
pinned: pinned === true,
|
|
94
469
|
sharedWith: Array.isArray(sharedWith) ? sharedWith : undefined,
|
|
95
470
|
})
|
|
96
|
-
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.`
|
|
97
472
|
}
|
|
98
473
|
|
|
99
|
-
if (
|
|
100
|
-
const found = memDb.get(
|
|
101
|
-
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}`
|
|
102
477
|
return formatEntry(found)
|
|
103
478
|
}
|
|
104
479
|
|
|
105
|
-
if (
|
|
106
|
-
const queries =
|
|
480
|
+
if (resolvedAction === 'search') {
|
|
481
|
+
const queries = queryText ? await expandQuery(queryText) : [keyText]
|
|
107
482
|
const allResults: MemoryEntry[] = []
|
|
108
483
|
const seenIds = new Set<string>()
|
|
109
484
|
for (const q of queries) {
|
|
@@ -114,23 +489,103 @@ async function executeMemoryAction(input: any, ctx: any) {
|
|
|
114
489
|
}
|
|
115
490
|
}
|
|
116
491
|
}
|
|
117
|
-
|
|
118
|
-
|
|
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')
|
|
119
496
|
}
|
|
120
497
|
|
|
121
|
-
if (
|
|
498
|
+
if (resolvedAction === 'list') {
|
|
122
499
|
const results = filterScope(memDb.list(undefined, maxPerLookup))
|
|
123
|
-
|
|
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.`
|
|
124
566
|
}
|
|
125
567
|
|
|
126
|
-
if (
|
|
127
|
-
|
|
568
|
+
if (resolvedAction === 'link' || resolvedAction === 'unlink') {
|
|
569
|
+
if (!memoryId) return `Memory ${resolvedAction} requires id or key.`
|
|
570
|
+
const found = memDb.get(memoryId)
|
|
128
571
|
if (!found || !canMutateMemory(found)) return 'Memory not found or access denied.'
|
|
129
|
-
|
|
130
|
-
|
|
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)
|
|
131
586
|
}
|
|
132
587
|
|
|
133
|
-
return `Unknown action "${
|
|
588
|
+
return `Unknown action "${resolvedAction}".`
|
|
134
589
|
}
|
|
135
590
|
|
|
136
591
|
/**
|
|
@@ -143,8 +598,7 @@ const MemoryPlugin: Plugin = {
|
|
|
143
598
|
getAgentContext: async (ctx) => {
|
|
144
599
|
const agentId = ctx.session.agentId
|
|
145
600
|
if (!agentId) return null
|
|
146
|
-
|
|
147
|
-
try { syncSessionArchiveMemory(ctx.session) } catch { /* archive sync is best-effort */ }
|
|
601
|
+
if (!shouldInjectMemoryContext(ctx.message || '')) return null
|
|
148
602
|
|
|
149
603
|
const memDb = getMemoryDb()
|
|
150
604
|
const memoryQuerySeed = [
|
|
@@ -166,30 +620,37 @@ const MemoryPlugin: Plugin = {
|
|
|
166
620
|
|
|
167
621
|
const pinned = memDb.listPinned(agentId, 5)
|
|
168
622
|
const pinnedLines = pinned
|
|
169
|
-
.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
|
+
})
|
|
170
629
|
.map(formatMemoryLine)
|
|
171
630
|
|
|
172
631
|
const relevantSlice = Math.max(2, 6 - pinnedLines.length)
|
|
173
632
|
const relevantLookup = memDb.searchWithLinked(memoryQuerySeed, agentId, 1, 10, 14)
|
|
174
|
-
const relevant = relevantLookup.entries.slice(0, relevantSlice)
|
|
175
633
|
const recent = memDb.list(agentId, 12).slice(0, 6)
|
|
176
|
-
const relevantByTier = partitionMemoriesByTier(
|
|
634
|
+
const relevantByTier = partitionMemoriesByTier(relevantLookup.entries)
|
|
177
635
|
const recentByTier = partitionMemoriesByTier(recent)
|
|
178
636
|
|
|
179
637
|
const relevantLines = relevantByTier.durable
|
|
180
|
-
.filter((m) => {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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)
|
|
185
645
|
.map(formatMemoryLine)
|
|
186
646
|
|
|
187
647
|
const recentLines = recentByTier.durable
|
|
188
|
-
.filter((m) => {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
+
})
|
|
193
654
|
.map(formatMemoryLine)
|
|
194
655
|
|
|
195
656
|
const parts: string[] = []
|
|
@@ -199,21 +660,15 @@ const MemoryPlugin: Plugin = {
|
|
|
199
660
|
if (relevantLines.length) {
|
|
200
661
|
parts.push(['## Relevant Memory Hits', 'These memories were retrieved by relevance for the current objective.', ...relevantLines].join('\n'))
|
|
201
662
|
}
|
|
202
|
-
if (archiveLines.length) {
|
|
203
|
-
parts.push(['## Session Archive Hits', 'Past conversation snapshots that may restore context from older chats.', ...archiveLines].join('\n'))
|
|
204
|
-
}
|
|
205
663
|
if (recentLines.length) {
|
|
206
664
|
parts.push(['## Recent Memory Notes', 'Recent durable notes that may still apply.', ...recentLines].join('\n'))
|
|
207
665
|
}
|
|
208
|
-
if (recentArchiveLines.length) {
|
|
209
|
-
parts.push(['## Recent Session Archives', 'Recently synced conversation archives you can search instead of relying on stale live context.', ...recentArchiveLines].join('\n'))
|
|
210
|
-
}
|
|
211
666
|
|
|
212
667
|
// Memory Policy
|
|
213
668
|
parts.push([
|
|
214
669
|
'## My Memory',
|
|
215
|
-
'I have long-term memory that persists across conversations. I use it
|
|
216
|
-
'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.',
|
|
217
672
|
'',
|
|
218
673
|
'**Things worth remembering:**',
|
|
219
674
|
'- What the user likes, dislikes, or has corrected me on',
|
|
@@ -231,8 +686,8 @@ const MemoryPlugin: Plugin = {
|
|
|
231
686
|
'',
|
|
232
687
|
'**Good habits:**',
|
|
233
688
|
'- Give memories clear titles ("User prefers dark mode" not "Note 1")',
|
|
234
|
-
'- Use categories:
|
|
235
|
-
'-
|
|
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',
|
|
236
691
|
'- Check what I already know before storing something new',
|
|
237
692
|
'- When I learn something that corrects old knowledge, update or remove the old memory',
|
|
238
693
|
].join('\n'))
|
|
@@ -269,15 +724,14 @@ const MemoryPlugin: Plugin = {
|
|
|
269
724
|
} catch { /* breadcrumbs are best-effort */ }
|
|
270
725
|
},
|
|
271
726
|
afterChatTurn: (ctx) => {
|
|
272
|
-
if (ctx.internal) return
|
|
273
|
-
if (ctx.source !== 'chat' && ctx.source !== 'connector') return
|
|
274
727
|
const agentId = ctx.session.agentId
|
|
275
728
|
if (!agentId) return
|
|
276
729
|
const msg = (ctx.message || '').trim()
|
|
277
730
|
const resp = (ctx.response || '').trim()
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
731
|
+
const shouldCapture = ctx.internal
|
|
732
|
+
? shouldAutoCaptureAutonomousTurn(ctx)
|
|
733
|
+
: ((ctx.source === 'chat' || ctx.source === 'connector') && shouldAutoCaptureMemoryTurn(msg, resp))
|
|
734
|
+
if (!shouldCapture) return
|
|
281
735
|
const now = Date.now()
|
|
282
736
|
const last = typeof ctx.session.lastAutoMemoryAt === 'number' ? ctx.session.lastAutoMemoryAt : 0
|
|
283
737
|
if (last > 0 && now - last < 5 * 60 * 1000) return
|
|
@@ -285,30 +739,55 @@ const MemoryPlugin: Plugin = {
|
|
|
285
739
|
const memDb = getMemoryDb()
|
|
286
740
|
const compactMessage = msg.replace(/\s+/g, ' ').slice(0, 220)
|
|
287
741
|
const compactResponse = resp.replace(/\s+/g, ' ').slice(0, 700)
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
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
|
+
})
|
|
291
763
|
ctx.session.lastAutoMemoryAt = now
|
|
292
764
|
} catch { /* auto-memory is best-effort */ }
|
|
293
765
|
},
|
|
294
766
|
getCapabilityDescription: () => 'I have long-term memory (`memory_tool`) — I can remember things across conversations and recall them when needed.',
|
|
295
767
|
getOperatingGuidance: () => [
|
|
296
|
-
'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.',
|
|
297
772
|
'For open goals, form a hypothesis and execute — do not keep re-asking broad questions.',
|
|
298
773
|
],
|
|
299
774
|
} as PluginHooks,
|
|
300
775
|
tools: [
|
|
301
776
|
{
|
|
302
777
|
name: 'memory_tool',
|
|
303
|
-
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.',
|
|
304
779
|
parameters: {
|
|
305
780
|
type: 'object',
|
|
306
781
|
properties: {
|
|
307
|
-
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' },
|
|
308
784
|
key: { type: 'string' },
|
|
785
|
+
title: { type: 'string' },
|
|
309
786
|
value: { type: 'string' },
|
|
310
787
|
category: { type: 'string' },
|
|
311
788
|
query: { type: 'string' },
|
|
789
|
+
sources: { type: 'array', items: { type: 'string', enum: ['durable', 'working', 'archive', 'all'] } },
|
|
790
|
+
targetIds: { type: 'array', items: { type: 'string' } },
|
|
312
791
|
scope: { type: 'string', enum: ['auto', 'all', 'global', 'shared', 'agent', 'session', 'project'] },
|
|
313
792
|
},
|
|
314
793
|
required: ['action']
|