@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.
Files changed (281) hide show
  1. package/README.md +12 -14
  2. package/next.config.ts +13 -2
  3. package/package.json +4 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +9 -0
  5. package/src/app/api/agents/route.ts +4 -0
  6. package/src/app/api/agents/thread-route.test.ts +133 -0
  7. package/src/app/api/approvals/route.test.ts +148 -0
  8. package/src/app/api/canvas/[sessionId]/route.ts +3 -1
  9. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
  10. package/src/app/api/chats/[id]/devserver/route.ts +48 -7
  11. package/src/app/api/chats/[id]/messages/route.ts +42 -18
  12. package/src/app/api/chats/[id]/route.ts +1 -1
  13. package/src/app/api/chats/[id]/stop/route.ts +5 -4
  14. package/src/app/api/chats/route.ts +23 -2
  15. package/src/app/api/clawhub/install/route.ts +28 -8
  16. package/src/app/api/connectors/[id]/route.ts +46 -3
  17. package/src/app/api/connectors/route.ts +12 -8
  18. package/src/app/api/external-agents/route.test.ts +165 -0
  19. package/src/app/api/gateways/[id]/health/route.ts +27 -12
  20. package/src/app/api/gateways/[id]/route.ts +2 -0
  21. package/src/app/api/gateways/health-route.test.ts +135 -0
  22. package/src/app/api/gateways/route.ts +2 -0
  23. package/src/app/api/mcp-servers/route.test.ts +130 -0
  24. package/src/app/api/openclaw/deploy/route.ts +38 -5
  25. package/src/app/api/plugins/install/route.ts +46 -6
  26. package/src/app/api/plugins/marketplace/route.ts +48 -15
  27. package/src/app/api/preview-server/route.ts +26 -11
  28. package/src/app/api/projects/[id]/route.ts +6 -2
  29. package/src/app/api/projects/route.ts +4 -3
  30. package/src/app/api/schedules/[id]/run/route.ts +4 -0
  31. package/src/app/api/schedules/route.test.ts +86 -0
  32. package/src/app/api/schedules/route.ts +6 -1
  33. package/src/app/api/secrets/[id]/route.ts +1 -0
  34. package/src/app/api/secrets/route.ts +2 -1
  35. package/src/app/api/settings/route.ts +2 -0
  36. package/src/app/api/setup/check-provider/route.test.ts +19 -0
  37. package/src/app/api/setup/check-provider/route.ts +40 -10
  38. package/src/app/api/skills/[id]/route.ts +12 -0
  39. package/src/app/api/skills/import/route.ts +14 -12
  40. package/src/app/api/skills/route.ts +13 -1
  41. package/src/app/api/tasks/[id]/route.ts +10 -1
  42. package/src/app/api/tasks/import/github/route.test.ts +65 -0
  43. package/src/app/api/tasks/import/github/route.ts +337 -0
  44. package/src/app/api/wallets/[id]/approve/route.ts +17 -3
  45. package/src/app/api/wallets/[id]/route.ts +79 -33
  46. package/src/app/api/wallets/[id]/send/route.ts +19 -33
  47. package/src/app/api/wallets/route.ts +78 -61
  48. package/src/app/api/webhooks/[id]/route.ts +33 -6
  49. package/src/app/api/webhooks/route.test.ts +272 -0
  50. package/src/cli/index.js +1 -0
  51. package/src/cli/spec.js +1 -0
  52. package/src/components/agents/agent-card.tsx +9 -2
  53. package/src/components/agents/agent-chat-list.tsx +18 -2
  54. package/src/components/agents/agent-list.tsx +1 -0
  55. package/src/components/agents/agent-sheet.tsx +257 -38
  56. package/src/components/agents/inspector-panel.tsx +41 -0
  57. package/src/components/canvas/canvas-panel.tsx +236 -65
  58. package/src/components/chat/chat-area.tsx +36 -19
  59. package/src/components/chat/chat-card.tsx +36 -13
  60. package/src/components/chat/chat-header.tsx +48 -16
  61. package/src/components/chat/chat-list.tsx +28 -4
  62. package/src/components/chat/checkpoint-timeline.tsx +50 -34
  63. package/src/components/chat/delegation-banner.test.ts +14 -1
  64. package/src/components/chat/delegation-banner.tsx +1 -1
  65. package/src/components/chat/message-bubble.tsx +208 -145
  66. package/src/components/chat/message-list.tsx +48 -19
  67. package/src/components/chatrooms/chatroom-message.tsx +2 -2
  68. package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
  69. package/src/components/connectors/connector-health.tsx +1 -1
  70. package/src/components/connectors/connector-list.tsx +7 -2
  71. package/src/components/connectors/connector-sheet.tsx +337 -148
  72. package/src/components/gateways/gateway-sheet.tsx +2 -2
  73. package/src/components/layout/app-layout.tsx +40 -23
  74. package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
  75. package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
  76. package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
  77. package/src/components/plugins/plugin-list.tsx +45 -9
  78. package/src/components/plugins/plugin-sheet.tsx +55 -7
  79. package/src/components/projects/project-detail.tsx +217 -0
  80. package/src/components/projects/project-sheet.tsx +176 -4
  81. package/src/components/providers/provider-list.tsx +2 -1
  82. package/src/components/providers/provider-sheet.tsx +21 -2
  83. package/src/components/schedules/schedule-card.tsx +25 -1
  84. package/src/components/schedules/schedule-sheet.tsx +44 -2
  85. package/src/components/secrets/secret-sheet.tsx +21 -2
  86. package/src/components/shared/agent-switch-dialog.tsx +12 -1
  87. package/src/components/shared/bottom-sheet.tsx +13 -3
  88. package/src/components/shared/command-palette.tsx +8 -1
  89. package/src/components/shared/confirm-dialog.tsx +19 -4
  90. package/src/components/shared/connector-platform-icon.test.ts +28 -0
  91. package/src/components/shared/connector-platform-icon.tsx +39 -6
  92. package/src/components/shared/settings/plugin-manager.tsx +29 -6
  93. package/src/components/shared/settings/section-capability-policy.tsx +45 -3
  94. package/src/components/shared/settings/section-voice.tsx +11 -3
  95. package/src/components/skills/skill-list.tsx +25 -0
  96. package/src/components/skills/skill-sheet.tsx +84 -12
  97. package/src/components/tasks/approvals-panel.tsx +289 -34
  98. package/src/components/tasks/task-board.tsx +410 -25
  99. package/src/components/tasks/task-card.tsx +66 -8
  100. package/src/components/tasks/task-sheet.tsx +16 -4
  101. package/src/components/ui/dialog.tsx +2 -2
  102. package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
  103. package/src/components/wallets/wallet-panel.tsx +435 -90
  104. package/src/components/wallets/wallet-section.tsx +198 -48
  105. package/src/components/webhooks/webhook-sheet.tsx +22 -2
  106. package/src/lib/approval-display.ts +20 -0
  107. package/src/lib/canvas-content.ts +198 -0
  108. package/src/lib/chat-artifact-summary.ts +165 -0
  109. package/src/lib/chat-display.test.ts +91 -0
  110. package/src/lib/chat-display.ts +58 -0
  111. package/src/lib/chat-streaming-state.test.ts +47 -1
  112. package/src/lib/chat-streaming-state.ts +42 -0
  113. package/src/lib/ollama-model.ts +10 -0
  114. package/src/lib/openclaw-endpoint.test.ts +8 -0
  115. package/src/lib/openclaw-endpoint.ts +6 -1
  116. package/src/lib/plugin-install-cors.ts +46 -0
  117. package/src/lib/plugin-sources.test.ts +43 -0
  118. package/src/lib/plugin-sources.ts +77 -0
  119. package/src/lib/providers/ollama.ts +16 -6
  120. package/src/lib/providers/openclaw.test.ts +54 -0
  121. package/src/lib/providers/openclaw.ts +127 -11
  122. package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
  123. package/src/lib/schedule-dedupe.test.ts +66 -1
  124. package/src/lib/schedule-dedupe.ts +169 -12
  125. package/src/lib/schedule-origin.test.ts +20 -0
  126. package/src/lib/schedule-origin.ts +15 -0
  127. package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
  128. package/src/lib/server/agent-availability.ts +16 -0
  129. package/src/lib/server/agent-runtime-config.ts +12 -4
  130. package/src/lib/server/agent-thread-session.test.ts +51 -0
  131. package/src/lib/server/agent-thread-session.ts +7 -0
  132. package/src/lib/server/approval-match.ts +205 -0
  133. package/src/lib/server/approvals-auto-approve.test.ts +538 -1
  134. package/src/lib/server/approvals.ts +214 -1
  135. package/src/lib/server/assistant-control.test.ts +29 -0
  136. package/src/lib/server/assistant-control.ts +23 -0
  137. package/src/lib/server/build-llm.test.ts +79 -0
  138. package/src/lib/server/build-llm.ts +14 -4
  139. package/src/lib/server/canvas-content.test.ts +32 -0
  140. package/src/lib/server/canvas-content.ts +6 -0
  141. package/src/lib/server/capability-router.test.ts +33 -0
  142. package/src/lib/server/capability-router.ts +80 -19
  143. package/src/lib/server/chat-execution-advanced.test.ts +651 -0
  144. package/src/lib/server/chat-execution-disabled.test.ts +94 -0
  145. package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
  146. package/src/lib/server/chat-execution.ts +378 -73
  147. package/src/lib/server/clawhub-client.test.ts +14 -8
  148. package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
  149. package/src/lib/server/connectors/manager.test.ts +1147 -0
  150. package/src/lib/server/connectors/manager.ts +461 -137
  151. package/src/lib/server/connectors/pairing.ts +26 -5
  152. package/src/lib/server/connectors/types.ts +2 -0
  153. package/src/lib/server/connectors/whatsapp.test.ts +134 -0
  154. package/src/lib/server/connectors/whatsapp.ts +271 -47
  155. package/src/lib/server/context-manager.ts +6 -1
  156. package/src/lib/server/daemon-state.ts +84 -47
  157. package/src/lib/server/data-dir.test.ts +37 -0
  158. package/src/lib/server/data-dir.ts +20 -1
  159. package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
  160. package/src/lib/server/devserver-launch.test.ts +60 -0
  161. package/src/lib/server/devserver-launch.ts +85 -0
  162. package/src/lib/server/elevenlabs.test.ts +247 -1
  163. package/src/lib/server/elevenlabs.ts +147 -43
  164. package/src/lib/server/ethereum.ts +590 -0
  165. package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
  166. package/src/lib/server/eval/agent-regression.test.ts +18 -1
  167. package/src/lib/server/eval/agent-regression.ts +383 -11
  168. package/src/lib/server/evm-swap.ts +475 -0
  169. package/src/lib/server/execution-log.ts +1 -0
  170. package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
  171. package/src/lib/server/heartbeat-service.ts +20 -11
  172. package/src/lib/server/heartbeat-wake.test.ts +112 -0
  173. package/src/lib/server/heartbeat-wake.ts +338 -57
  174. package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
  175. package/src/lib/server/main-agent-loop.test.ts +260 -0
  176. package/src/lib/server/main-agent-loop.ts +559 -14
  177. package/src/lib/server/mcp-client.test.ts +16 -0
  178. package/src/lib/server/mcp-client.ts +25 -0
  179. package/src/lib/server/memory-integration.test.ts +719 -0
  180. package/src/lib/server/memory-policy.test.ts +43 -0
  181. package/src/lib/server/memory-policy.ts +132 -0
  182. package/src/lib/server/memory-tiers.test.ts +60 -0
  183. package/src/lib/server/memory-tiers.ts +16 -0
  184. package/src/lib/server/ollama-runtime.ts +58 -0
  185. package/src/lib/server/openclaw-deploy.test.ts +109 -1
  186. package/src/lib/server/openclaw-deploy.ts +557 -81
  187. package/src/lib/server/openclaw-gateway.test.ts +131 -0
  188. package/src/lib/server/openclaw-gateway.ts +10 -4
  189. package/src/lib/server/openclaw-health.test.ts +35 -0
  190. package/src/lib/server/openclaw-health.ts +215 -47
  191. package/src/lib/server/orchestrator-lg.ts +3 -2
  192. package/src/lib/server/orchestrator.ts +2 -0
  193. package/src/lib/server/plugins-advanced.test.ts +351 -0
  194. package/src/lib/server/plugins.ts +211 -6
  195. package/src/lib/server/project-context.ts +162 -0
  196. package/src/lib/server/project-utils.ts +150 -0
  197. package/src/lib/server/queue-advanced.test.ts +528 -0
  198. package/src/lib/server/queue-followups.test.ts +409 -2
  199. package/src/lib/server/queue-reconcile.test.ts +128 -0
  200. package/src/lib/server/queue.ts +527 -68
  201. package/src/lib/server/scheduler.ts +29 -1
  202. package/src/lib/server/session-note.test.ts +36 -0
  203. package/src/lib/server/session-note.ts +42 -0
  204. package/src/lib/server/session-run-manager.ts +83 -4
  205. package/src/lib/server/session-tools/canvas.ts +14 -12
  206. package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
  207. package/src/lib/server/session-tools/connector.test.ts +138 -0
  208. package/src/lib/server/session-tools/connector.ts +366 -54
  209. package/src/lib/server/session-tools/context.ts +17 -3
  210. package/src/lib/server/session-tools/crud.ts +484 -84
  211. package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
  212. package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
  213. package/src/lib/server/session-tools/delegate.ts +102 -10
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
  215. package/src/lib/server/session-tools/discovery.ts +80 -12
  216. package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
  217. package/src/lib/server/session-tools/file.ts +43 -4
  218. package/src/lib/server/session-tools/human-loop.ts +35 -5
  219. package/src/lib/server/session-tools/index.ts +44 -9
  220. package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
  221. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
  222. package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
  223. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
  224. package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
  225. package/src/lib/server/session-tools/memory.test.ts +93 -0
  226. package/src/lib/server/session-tools/memory.ts +554 -75
  227. package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
  228. package/src/lib/server/session-tools/platform-access.test.ts +58 -0
  229. package/src/lib/server/session-tools/platform.ts +60 -19
  230. package/src/lib/server/session-tools/plugin-creator.ts +57 -1
  231. package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
  232. package/src/lib/server/session-tools/schedule.ts +6 -1
  233. package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
  234. package/src/lib/server/session-tools/shell.ts +22 -3
  235. package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
  236. package/src/lib/server/session-tools/wallet.ts +1374 -139
  237. package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
  238. package/src/lib/server/session-tools/web.ts +621 -70
  239. package/src/lib/server/skill-discovery.ts +128 -0
  240. package/src/lib/server/skill-eligibility.test.ts +84 -0
  241. package/src/lib/server/skill-eligibility.ts +95 -0
  242. package/src/lib/server/skill-prompt-budget.test.ts +102 -0
  243. package/src/lib/server/skill-prompt-budget.ts +125 -0
  244. package/src/lib/server/skills-normalize.test.ts +54 -0
  245. package/src/lib/server/skills-normalize.ts +372 -26
  246. package/src/lib/server/solana.ts +214 -29
  247. package/src/lib/server/storage.ts +65 -36
  248. package/src/lib/server/stream-agent-chat.test.ts +437 -2
  249. package/src/lib/server/stream-agent-chat.ts +957 -79
  250. package/src/lib/server/system-events.ts +1 -1
  251. package/src/lib/server/tool-aliases.ts +2 -0
  252. package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
  253. package/src/lib/server/tool-capability-policy.test.ts +24 -0
  254. package/src/lib/server/tool-capability-policy.ts +29 -1
  255. package/src/lib/server/tool-loop-detection.test.ts +105 -0
  256. package/src/lib/server/tool-loop-detection.ts +260 -0
  257. package/src/lib/server/tool-planning.test.ts +44 -0
  258. package/src/lib/server/tool-planning.ts +271 -0
  259. package/src/lib/server/wallet-execution.test.ts +198 -0
  260. package/src/lib/server/wallet-portfolio.test.ts +98 -0
  261. package/src/lib/server/wallet-portfolio.ts +724 -0
  262. package/src/lib/server/wallet-service.test.ts +57 -0
  263. package/src/lib/server/wallet-service.ts +213 -0
  264. package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
  265. package/src/lib/server/watch-jobs.ts +17 -2
  266. package/src/lib/server/workspace-context.ts +111 -0
  267. package/src/lib/skill-save-payload.test.ts +39 -0
  268. package/src/lib/skill-save-payload.ts +37 -0
  269. package/src/lib/tasks.ts +28 -0
  270. package/src/lib/tool-definitions.ts +2 -1
  271. package/src/lib/tool-event-summary.test.ts +30 -0
  272. package/src/lib/tool-event-summary.ts +37 -0
  273. package/src/lib/validation/schemas.ts +1 -0
  274. package/src/lib/wallet-transactions.test.ts +75 -0
  275. package/src/lib/wallet-transactions.ts +43 -0
  276. package/src/lib/wallet.test.ts +17 -0
  277. package/src/lib/wallet.ts +183 -0
  278. package/src/proxy.test.ts +31 -0
  279. package/src/proxy.ts +34 -2
  280. package/src/stores/use-chat-store.ts +15 -1
  281. 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
- async function executeMemoryAction(input: any, ctx: any) {
25
- const normalized = normalizeToolInputArgs((input ?? {}) as Record<string, unknown>)
26
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
- const n = normalized as Record<string, any>
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, depth, linkedLimit, targetIds,
32
- tags, pinned, sharedWith
298
+ linkedMemoryIds, targetIds,
299
+ pinned, sharedWith,
33
300
  } = n
34
- const category = typeof n.category === 'string' ? n.category : 'note'
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 = ctx && typeof ctx === 'object' && Array.isArray(ctx.messages) ? ctx : null
45
- const rawScope = typeof scope === 'string' ? scope : 'auto'
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()) ? projectRoot.trim() : ((project && typeof project === 'object' && 'rootPath' in project && typeof (project as Record<string, unknown>).rootPath === 'string') ? (project as Record<string, unknown>).rootPath as string : null),
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
- if ((action === 'search' || action === 'list') && currentSession) {
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: any) => {
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: any) => `${r.type}:${r.path || r.title || r.type}`).join(', ')}`
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 (action === 'store') {
79
- let storedImage: any = null
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: category || 'note',
87
- title: key,
88
- content: value || '',
89
- references: Array.isArray(references) ? references : [],
90
- filePaths: filePaths as any,
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 "${key}" (id: ${entry.id})`
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 (action === 'get') {
100
- const found = memDb.get(key)
101
- if (!found || !canReadMemory(found)) return `Memory not found or access denied: ${key}`
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 (action === 'search') {
106
- const queries = query ? await expandQuery(query) : [key || '']
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
- if (!allResults.length) return 'No memories found.'
118
- return allResults.slice(0, maxPerLookup).map(formatEntry).join('\n')
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 (action === 'list') {
498
+ if (resolvedAction === 'list') {
122
499
  const results = filterScope(memDb.list(undefined, maxPerLookup))
123
- return results.length ? results.map(formatEntry).join('\n') : 'No memories stored yet.'
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 (action === 'delete') {
127
- const found = memDb.get(key)
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
- memDb.delete(key)
130
- return `Deleted memory "${key}"`
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 "${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) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
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(relevant)
634
+ const relevantByTier = partitionMemoriesByTier(relevantLookup.entries)
177
635
  const recentByTier = partitionMemoriesByTier(recent)
178
636
 
179
637
  const relevantLines = relevantByTier.durable
180
- .filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
181
- .map(formatMemoryLine)
182
-
183
- const archiveLines = relevantByTier.archive
184
- .filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
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) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
189
- .map(formatMemoryLine)
190
-
191
- const recentArchiveLines = recentByTier.archive
192
- .filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
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 naturally I don\'t wait to be asked to remember things.',
216
- 'Memory tiers: working memory is short-lived, durable memory stores stable facts and decisions, and session archives capture older conversation context for search.',
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: preference, fact, learning, project, identity, decision',
235
- '- Search session archives before assuming older conversation context is still in the live chat history',
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
- if (msg.length < 20 || resp.length < 40) return
279
- if (/^(ok|okay|cool|thanks|thx|got it|nice)[.! ]*$/i.test(msg)) return
280
- if (resp === 'HEARTBEAT_OK') return
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 autoTitle = `[auto] ${compactMessage.slice(0, 90)}`
289
- const content = `source: ${ctx.source}\nuser_request: ${compactMessage}\nassistant_outcome: ${compactResponse}`
290
- memDb.add({ agentId, sessionId: ctx.session.id, category: 'execution', title: autoTitle, content })
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: search before major tasks, store concise notes after meaningful steps. Platform preloads context each turn.',
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. Use to store and recall facts across all conversations.',
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']