@swarmclawai/swarmclaw 0.7.8 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/README.md +12 -15
  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 +22 -2
  15. package/src/app/api/clawhub/install/route.ts +28 -8
  16. package/src/app/api/connectors/[id]/route.ts +26 -1
  17. package/src/app/api/external-agents/route.test.ts +165 -0
  18. package/src/app/api/gateways/[id]/health/route.ts +27 -12
  19. package/src/app/api/gateways/[id]/route.ts +2 -0
  20. package/src/app/api/gateways/health-route.test.ts +135 -0
  21. package/src/app/api/gateways/route.ts +2 -0
  22. package/src/app/api/mcp-servers/route.test.ts +130 -0
  23. package/src/app/api/openclaw/deploy/route.ts +38 -5
  24. package/src/app/api/plugins/install/route.ts +46 -6
  25. package/src/app/api/plugins/marketplace/route.ts +48 -15
  26. package/src/app/api/preview-server/route.ts +26 -11
  27. package/src/app/api/schedules/[id]/run/route.ts +4 -0
  28. package/src/app/api/schedules/route.test.ts +86 -0
  29. package/src/app/api/schedules/route.ts +6 -1
  30. package/src/app/api/setup/check-provider/route.test.ts +19 -0
  31. package/src/app/api/setup/check-provider/route.ts +40 -10
  32. package/src/app/api/skills/[id]/route.ts +12 -0
  33. package/src/app/api/skills/import/route.ts +14 -12
  34. package/src/app/api/skills/route.ts +13 -1
  35. package/src/app/api/tasks/[id]/route.ts +10 -1
  36. package/src/app/api/tasks/import/github/route.test.ts +65 -0
  37. package/src/app/api/tasks/import/github/route.ts +337 -0
  38. package/src/app/api/wallets/[id]/approve/route.ts +17 -3
  39. package/src/app/api/wallets/[id]/route.ts +79 -33
  40. package/src/app/api/wallets/[id]/send/route.ts +19 -33
  41. package/src/app/api/wallets/route.ts +78 -61
  42. package/src/app/api/webhooks/[id]/route.ts +33 -6
  43. package/src/app/api/webhooks/route.test.ts +272 -0
  44. package/src/cli/index.js +1 -0
  45. package/src/cli/spec.js +1 -0
  46. package/src/components/agents/agent-card.tsx +9 -2
  47. package/src/components/agents/agent-chat-list.tsx +18 -2
  48. package/src/components/agents/agent-list.tsx +1 -0
  49. package/src/components/agents/agent-sheet.tsx +73 -24
  50. package/src/components/agents/inspector-panel.tsx +41 -0
  51. package/src/components/canvas/canvas-panel.tsx +236 -65
  52. package/src/components/chat/chat-card.tsx +36 -13
  53. package/src/components/chat/chat-header.tsx +44 -16
  54. package/src/components/chat/chat-list.tsx +28 -4
  55. package/src/components/chat/checkpoint-timeline.tsx +50 -34
  56. package/src/components/chat/message-bubble.tsx +208 -145
  57. package/src/components/chat/message-list.tsx +48 -19
  58. package/src/components/chatrooms/chatroom-message.tsx +2 -2
  59. package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
  60. package/src/components/connectors/connector-health.tsx +1 -1
  61. package/src/components/connectors/connector-list.tsx +7 -2
  62. package/src/components/connectors/connector-sheet.tsx +337 -148
  63. package/src/components/gateways/gateway-sheet.tsx +2 -2
  64. package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
  65. package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
  66. package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
  67. package/src/components/plugins/plugin-list.tsx +45 -9
  68. package/src/components/plugins/plugin-sheet.tsx +55 -7
  69. package/src/components/providers/provider-list.tsx +2 -1
  70. package/src/components/providers/provider-sheet.tsx +21 -2
  71. package/src/components/schedules/schedule-card.tsx +25 -1
  72. package/src/components/schedules/schedule-sheet.tsx +44 -2
  73. package/src/components/secrets/secret-sheet.tsx +21 -2
  74. package/src/components/shared/agent-switch-dialog.tsx +12 -1
  75. package/src/components/shared/bottom-sheet.tsx +13 -3
  76. package/src/components/shared/command-palette.tsx +8 -1
  77. package/src/components/shared/confirm-dialog.tsx +19 -4
  78. package/src/components/shared/connector-platform-icon.test.ts +28 -0
  79. package/src/components/shared/connector-platform-icon.tsx +39 -6
  80. package/src/components/shared/settings/plugin-manager.tsx +29 -6
  81. package/src/components/shared/settings/section-capability-policy.tsx +7 -3
  82. package/src/components/skills/skill-list.tsx +25 -0
  83. package/src/components/skills/skill-sheet.tsx +84 -12
  84. package/src/components/tasks/approvals-panel.tsx +191 -95
  85. package/src/components/tasks/task-board.tsx +273 -2
  86. package/src/components/tasks/task-card.tsx +38 -9
  87. package/src/components/ui/dialog.tsx +2 -2
  88. package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
  89. package/src/components/wallets/wallet-panel.tsx +435 -90
  90. package/src/components/wallets/wallet-section.tsx +198 -48
  91. package/src/components/webhooks/webhook-sheet.tsx +22 -2
  92. package/src/lib/approval-display.ts +20 -0
  93. package/src/lib/canvas-content.ts +198 -0
  94. package/src/lib/chat-artifact-summary.ts +165 -0
  95. package/src/lib/chat-display.test.ts +91 -0
  96. package/src/lib/chat-display.ts +58 -0
  97. package/src/lib/chat-streaming-state.test.ts +47 -1
  98. package/src/lib/chat-streaming-state.ts +42 -0
  99. package/src/lib/ollama-model.ts +10 -0
  100. package/src/lib/openclaw-endpoint.test.ts +8 -0
  101. package/src/lib/openclaw-endpoint.ts +6 -1
  102. package/src/lib/plugin-install-cors.ts +46 -0
  103. package/src/lib/plugin-sources.test.ts +43 -0
  104. package/src/lib/plugin-sources.ts +77 -0
  105. package/src/lib/providers/ollama.ts +16 -6
  106. package/src/lib/providers/openclaw.test.ts +54 -0
  107. package/src/lib/providers/openclaw.ts +127 -11
  108. package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
  109. package/src/lib/schedule-dedupe.test.ts +66 -1
  110. package/src/lib/schedule-dedupe.ts +169 -12
  111. package/src/lib/schedule-origin.test.ts +20 -0
  112. package/src/lib/schedule-origin.ts +15 -0
  113. package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
  114. package/src/lib/server/agent-availability.ts +16 -0
  115. package/src/lib/server/agent-runtime-config.ts +12 -4
  116. package/src/lib/server/agent-thread-session.test.ts +51 -0
  117. package/src/lib/server/agent-thread-session.ts +7 -0
  118. package/src/lib/server/approval-match.ts +205 -0
  119. package/src/lib/server/approvals-auto-approve.test.ts +538 -1
  120. package/src/lib/server/approvals.ts +214 -1
  121. package/src/lib/server/assistant-control.test.ts +29 -0
  122. package/src/lib/server/assistant-control.ts +23 -0
  123. package/src/lib/server/build-llm.test.ts +79 -0
  124. package/src/lib/server/build-llm.ts +14 -4
  125. package/src/lib/server/canvas-content.test.ts +32 -0
  126. package/src/lib/server/canvas-content.ts +6 -0
  127. package/src/lib/server/capability-router.test.ts +11 -0
  128. package/src/lib/server/capability-router.ts +26 -1
  129. package/src/lib/server/chat-execution-advanced.test.ts +651 -0
  130. package/src/lib/server/chat-execution-disabled.test.ts +94 -0
  131. package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
  132. package/src/lib/server/chat-execution.ts +353 -72
  133. package/src/lib/server/clawhub-client.test.ts +14 -8
  134. package/src/lib/server/connectors/manager.test.ts +1147 -0
  135. package/src/lib/server/connectors/manager.ts +362 -63
  136. package/src/lib/server/connectors/pairing.ts +26 -5
  137. package/src/lib/server/connectors/types.ts +2 -0
  138. package/src/lib/server/connectors/whatsapp.test.ts +134 -0
  139. package/src/lib/server/connectors/whatsapp.ts +271 -47
  140. package/src/lib/server/context-manager.ts +6 -1
  141. package/src/lib/server/daemon-state.ts +1 -1
  142. package/src/lib/server/data-dir.test.ts +37 -0
  143. package/src/lib/server/data-dir.ts +20 -1
  144. package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
  145. package/src/lib/server/devserver-launch.test.ts +60 -0
  146. package/src/lib/server/devserver-launch.ts +85 -0
  147. package/src/lib/server/elevenlabs.test.ts +189 -1
  148. package/src/lib/server/elevenlabs.ts +147 -43
  149. package/src/lib/server/ethereum.ts +590 -0
  150. package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
  151. package/src/lib/server/eval/agent-regression.test.ts +18 -1
  152. package/src/lib/server/eval/agent-regression.ts +383 -11
  153. package/src/lib/server/evm-swap.ts +475 -0
  154. package/src/lib/server/execution-log.ts +1 -0
  155. package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
  156. package/src/lib/server/heartbeat-service.ts +15 -10
  157. package/src/lib/server/heartbeat-wake.test.ts +112 -0
  158. package/src/lib/server/heartbeat-wake.ts +338 -57
  159. package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
  160. package/src/lib/server/mcp-client.test.ts +16 -0
  161. package/src/lib/server/mcp-client.ts +25 -0
  162. package/src/lib/server/memory-integration.test.ts +719 -0
  163. package/src/lib/server/memory-policy.test.ts +43 -0
  164. package/src/lib/server/memory-policy.ts +132 -0
  165. package/src/lib/server/memory-tiers.test.ts +60 -0
  166. package/src/lib/server/memory-tiers.ts +16 -0
  167. package/src/lib/server/ollama-runtime.ts +58 -0
  168. package/src/lib/server/openclaw-deploy.test.ts +109 -1
  169. package/src/lib/server/openclaw-deploy.ts +557 -81
  170. package/src/lib/server/openclaw-gateway.test.ts +131 -0
  171. package/src/lib/server/openclaw-gateway.ts +10 -4
  172. package/src/lib/server/openclaw-health.test.ts +35 -0
  173. package/src/lib/server/openclaw-health.ts +215 -47
  174. package/src/lib/server/orchestrator-lg.ts +2 -2
  175. package/src/lib/server/plugins-advanced.test.ts +351 -0
  176. package/src/lib/server/plugins.ts +205 -5
  177. package/src/lib/server/queue-advanced.test.ts +528 -0
  178. package/src/lib/server/queue-followups.test.ts +262 -0
  179. package/src/lib/server/queue-reconcile.test.ts +128 -0
  180. package/src/lib/server/queue.ts +293 -61
  181. package/src/lib/server/scheduler.ts +29 -1
  182. package/src/lib/server/session-note.test.ts +36 -0
  183. package/src/lib/server/session-note.ts +42 -0
  184. package/src/lib/server/session-run-manager.ts +52 -4
  185. package/src/lib/server/session-tools/canvas.ts +14 -12
  186. package/src/lib/server/session-tools/connector.test.ts +138 -0
  187. package/src/lib/server/session-tools/connector.ts +348 -61
  188. package/src/lib/server/session-tools/context.ts +12 -3
  189. package/src/lib/server/session-tools/crud.ts +221 -10
  190. package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
  191. package/src/lib/server/session-tools/delegate.ts +64 -8
  192. package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
  193. package/src/lib/server/session-tools/discovery.ts +80 -12
  194. package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
  195. package/src/lib/server/session-tools/file.ts +43 -4
  196. package/src/lib/server/session-tools/human-loop.ts +35 -5
  197. package/src/lib/server/session-tools/index.ts +44 -9
  198. package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
  199. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
  200. package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
  201. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
  202. package/src/lib/server/session-tools/memory.test.ts +93 -0
  203. package/src/lib/server/session-tools/memory.ts +546 -79
  204. package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
  205. package/src/lib/server/session-tools/plugin-creator.ts +57 -1
  206. package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
  207. package/src/lib/server/session-tools/schedule.ts +6 -1
  208. package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
  209. package/src/lib/server/session-tools/shell.ts +22 -3
  210. package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
  211. package/src/lib/server/session-tools/wallet.ts +1374 -139
  212. package/src/lib/server/session-tools/web-inputs.test.ts +162 -1
  213. package/src/lib/server/session-tools/web.ts +468 -64
  214. package/src/lib/server/skill-discovery.ts +128 -0
  215. package/src/lib/server/skill-eligibility.test.ts +84 -0
  216. package/src/lib/server/skill-eligibility.ts +95 -0
  217. package/src/lib/server/skill-prompt-budget.test.ts +102 -0
  218. package/src/lib/server/skill-prompt-budget.ts +125 -0
  219. package/src/lib/server/skills-normalize.test.ts +54 -0
  220. package/src/lib/server/skills-normalize.ts +372 -26
  221. package/src/lib/server/solana.ts +214 -29
  222. package/src/lib/server/storage.ts +65 -36
  223. package/src/lib/server/stream-agent-chat.test.ts +419 -9
  224. package/src/lib/server/stream-agent-chat.ts +887 -83
  225. package/src/lib/server/system-events.ts +1 -1
  226. package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
  227. package/src/lib/server/tool-loop-detection.test.ts +105 -0
  228. package/src/lib/server/tool-loop-detection.ts +260 -0
  229. package/src/lib/server/tool-planning.ts +4 -2
  230. package/src/lib/server/wallet-execution.test.ts +198 -0
  231. package/src/lib/server/wallet-portfolio.test.ts +98 -0
  232. package/src/lib/server/wallet-portfolio.ts +724 -0
  233. package/src/lib/server/wallet-service.test.ts +57 -0
  234. package/src/lib/server/wallet-service.ts +213 -0
  235. package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
  236. package/src/lib/server/watch-jobs.ts +17 -2
  237. package/src/lib/server/workspace-context.ts +111 -0
  238. package/src/lib/skill-save-payload.test.ts +39 -0
  239. package/src/lib/skill-save-payload.ts +37 -0
  240. package/src/lib/tasks.ts +28 -0
  241. package/src/lib/tool-event-summary.test.ts +30 -0
  242. package/src/lib/tool-event-summary.ts +37 -0
  243. package/src/lib/validation/schemas.ts +1 -0
  244. package/src/lib/wallet-transactions.test.ts +75 -0
  245. package/src/lib/wallet-transactions.ts +43 -0
  246. package/src/lib/wallet.test.ts +17 -0
  247. package/src/lib/wallet.ts +183 -0
  248. package/src/proxy.test.ts +31 -0
  249. package/src/proxy.ts +34 -2
  250. package/src/stores/use-chat-store.ts +15 -1
  251. package/src/types/index.ts +210 -14
@@ -8,30 +8,321 @@ import {
8
8
  getMemoryLookupLimits,
9
9
  normalizeMemoryScopeMode,
10
10
  storeMemoryImageAsset,
11
+ type MemoryScopeFilter,
11
12
  } from '../memory-db'
12
13
  import { loadSettings } from '../storage'
13
14
  import { expandQuery } from '../query-expansion'
14
- import type { MemoryEntry, Plugin, PluginHooks } from '@/types'
15
+ import type { FileReference, MemoryEntry, MemoryImage, MemoryReference, Plugin, PluginHooks, Session } from '@/types'
15
16
  import type { ToolBuildContext } from './context'
16
17
  import { getPluginManager } from '../plugins'
17
18
  import { normalizeToolInputArgs } from './normalize-tool-args'
18
- import { partitionMemoriesByTier } from '../memory-tiers'
19
+ import { getMemoryTier, partitionMemoriesByTier, shouldHideFromDurableRecall } from '../memory-tiers'
19
20
  import { syncSessionArchiveMemory } from '../session-archive-memory'
21
+ import {
22
+ buildMemoryDoctorReport,
23
+ normalizeMemoryCategory,
24
+ shouldAutoCaptureMemoryTurn,
25
+ shouldInjectMemoryContext,
26
+ } from '../memory-policy'
20
27
 
21
28
  /**
22
29
  * Advanced Database-Backed Memory logic.
23
30
  */
24
- 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,13 +332,13 @@ async function executeMemoryAction(input: any, ctx: any) {
41
332
  : typeof ctx?.id === 'string'
42
333
  ? ctx.id
43
334
  : null
44
- const currentSession = ctx && typeof ctx === 'object' && Array.isArray(ctx.messages) ? ctx : null
335
+ const currentSession = isSessionContext(ctx) ? ctx : null
45
336
  const configuredScope = typeof ctx?.memoryScopeMode === 'string' ? ctx.memoryScopeMode : 'auto'
46
337
  const rawScope = typeof scope === 'string' ? scope : configuredScope
47
338
  const scopeMode = normalizeMemoryScopeMode(rawScope === 'shared' ? 'global' : rawScope)
48
339
  const rerankMode = rerank === 'semantic' || rerank === 'lexical' ? rerank : 'balanced'
49
340
 
50
- const scopeFilter = {
341
+ const scopeFilter: MemoryScopeFilter = {
51
342
  mode: scopeMode,
52
343
  agentId: currentAgentId,
53
344
  sessionId: (typeof scopeSessionId === 'string' && scopeSessionId.trim()) ? scopeSessionId.trim() : currentSessionId,
@@ -64,58 +355,130 @@ async function executeMemoryAction(input: any, ctx: any) {
64
355
 
65
356
  const limits = getMemoryLookupLimits(loadSettings())
66
357
  const maxPerLookup = limits.maxPerLookup
358
+ const searchSources = normalizeMemorySearchSources(n.sources)
359
+ const inputMetadata = n.metadata && typeof n.metadata === 'object' && !Array.isArray(n.metadata)
360
+ ? { ...(n.metadata as Record<string, unknown>) }
361
+ : {}
362
+ if (scopeMode === 'project' && scopeFilter.projectRoot && !inputMetadata.projectRoot) {
363
+ inputMetadata.projectRoot = scopeFilter.projectRoot
364
+ }
365
+
366
+ const buildCanonicalMetadata = (title: string, content: string, extra?: Record<string, unknown>) => {
367
+ const subjectKey = buildMemorySubjectKey(title, content)
368
+ return mergeMemoryMetadata(inputMetadata, {
369
+ ...extra,
370
+ subjectKey: subjectKey || undefined,
371
+ tier: extra?.tier,
372
+ supersededBy: extra?.supersededBy,
373
+ supersededReason: extra?.supersededReason,
374
+ supersededAt: extra?.supersededAt,
375
+ })
376
+ }
377
+
378
+ const findRelatedCanonicalCandidates = (title: string, content: string) => selectCanonicalMemoryCandidates({
379
+ memDb,
380
+ agentId: currentAgentId,
381
+ title,
382
+ content,
383
+ canReadMemory,
384
+ canMutateMemory,
385
+ scopeFilter,
386
+ })
387
+
388
+ const supersedeCompetingMemories = (targetId: string, title: string, content: string, related: CanonicalMemoryCandidate[]) => {
389
+ const subjectKey = buildMemorySubjectKey(title, content)
390
+ const seen = new Set<string>()
391
+ for (const candidate of related) {
392
+ const entry = candidate.entry
393
+ if (entry.id === targetId || seen.has(entry.id)) continue
394
+ seen.add(entry.id)
395
+ const nextMetadata = mergeMemoryMetadata(entry.metadata, {
396
+ subjectKey: subjectKey || undefined,
397
+ supersededBy: targetId,
398
+ supersededReason: 'canonical-upsert',
399
+ supersededAt: Date.now(),
400
+ tier: 'working',
401
+ })
402
+ memDb.update(entry.id, {
403
+ metadata: nextMetadata,
404
+ })
405
+ }
406
+ }
67
407
 
68
- if ((action === 'search' || action === 'list') && currentSession) {
408
+ if ((resolvedAction === 'search' || resolvedAction === 'list') && currentSession && (searchSources.has('archive') || searchSources.has('all'))) {
69
409
  try { syncSessionArchiveMemory(currentSession) } catch { /* archive sync is best-effort */ }
70
410
  }
71
411
 
72
- const formatEntry = (m: any) => {
412
+ const formatEntry = (m: MemoryEntry) => {
73
413
  let line = `[${m.id}] (${m.agentId ? `agent:${m.agentId}` : 'shared'}) ${m.category}/${m.title}: ${m.content}`
74
414
  if (m.reinforcementCount) line += ` (reinforced ×${m.reinforcementCount})`
75
415
  if (m.references?.length) {
76
- line += `\n refs: ${m.references.map((r: 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(', ')}`
77
417
  }
78
418
  if (m.imagePath) line += `\n image: ${m.imagePath}`
79
419
  if (m.linkedMemoryIds?.length) line += `\n linked: ${m.linkedMemoryIds.join(', ')}`
80
420
  return line
81
421
  }
82
422
 
83
- if (action === 'store') {
84
- 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
85
432
  if (imagePath && fs.existsSync(imagePath)) {
86
433
  storedImage = await storeMemoryImageAsset(imagePath, genId(6))
87
434
  }
88
- const metadata = n.metadata && typeof n.metadata === 'object' && !Array.isArray(n.metadata)
89
- ? { ...(n.metadata as Record<string, unknown>) }
90
- : {}
91
- if (scopeMode === 'project' && scopeFilter.projectRoot && !metadata.projectRoot) {
92
- metadata.projectRoot = scopeFilter.projectRoot
435
+ const normalizedCategory = normalizeMemoryCategory(requestedCategory || 'note', memoryTitle, storedValueText)
436
+ const related = findRelatedCanonicalCandidates(memoryTitle, storedValueText)
437
+ const canonicalTarget = related[0]?.entry || null
438
+ const canonicalMetadata = buildCanonicalMetadata(memoryTitle, storedValueText)
439
+ if (canonicalTarget) {
440
+ const updated = memDb.update(canonicalTarget.id, {
441
+ title: memoryTitle,
442
+ content: storedValueText,
443
+ category: normalizedCategory,
444
+ metadata: mergeMemoryMetadata(canonicalTarget.metadata, canonicalMetadata),
445
+ references: Array.isArray(references) ? references as MemoryReference[] : canonicalTarget.references,
446
+ filePaths: Array.isArray(filePaths) ? filePaths as FileReference[] : canonicalTarget.filePaths,
447
+ imagePath: storedImage?.path || canonicalTarget.imagePath,
448
+ linkedMemoryIds: normalizedLinkedMemoryIds || canonicalTarget.linkedMemoryIds,
449
+ pinned: typeof pinned === 'boolean' ? pinned : canonicalTarget.pinned,
450
+ sharedWith: Array.isArray(sharedWith) ? sharedWith : canonicalTarget.sharedWith,
451
+ })
452
+ if (updated) {
453
+ supersedeCompetingMemories(updated.id, memoryTitle, storedValueText, related)
454
+ return `Stored memory "${updated.title}" (id: ${updated.id}) in ${normalizedCategory} by updating the canonical entry. No further memory lookup is needed unless the user asked you to verify.`
455
+ }
93
456
  }
94
457
  const entry = memDb.add({
95
458
  agentId: scopeMode === 'global' ? null : currentAgentId,
96
459
  sessionId: ctx?.sessionId || null,
97
- category: category || 'note',
98
- title: key,
99
- content: value || '',
100
- metadata,
101
- references: Array.isArray(references) ? references : [],
102
- 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,
103
466
  imagePath: storedImage?.path || undefined,
104
- linkedMemoryIds,
467
+ linkedMemoryIds: normalizedLinkedMemoryIds,
105
468
  pinned: pinned === true,
106
469
  sharedWith: Array.isArray(sharedWith) ? sharedWith : undefined,
107
470
  })
108
- return `Stored memory "${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.`
109
472
  }
110
473
 
111
- if (action === 'get') {
112
- const found = memDb.get(key)
113
- 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}`
114
477
  return formatEntry(found)
115
478
  }
116
479
 
117
- if (action === 'search') {
118
- const queries = query ? await expandQuery(query) : [key || '']
480
+ if (resolvedAction === 'search') {
481
+ const queries = queryText ? await expandQuery(queryText) : [keyText]
119
482
  const allResults: MemoryEntry[] = []
120
483
  const seenIds = new Set<string>()
121
484
  for (const q of queries) {
@@ -126,23 +489,103 @@ async function executeMemoryAction(input: any, ctx: any) {
126
489
  }
127
490
  }
128
491
  }
129
- if (!allResults.length) return 'No memories found.'
130
- 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')
131
496
  }
132
497
 
133
- if (action === 'list') {
498
+ if (resolvedAction === 'list') {
134
499
  const results = filterScope(memDb.list(undefined, maxPerLookup))
135
- 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.`
136
566
  }
137
567
 
138
- if (action === 'delete') {
139
- 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)
140
571
  if (!found || !canMutateMemory(found)) return 'Memory not found or access denied.'
141
- memDb.delete(key)
142
- 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)
143
586
  }
144
587
 
145
- return `Unknown action "${action}".`
588
+ return `Unknown action "${resolvedAction}".`
146
589
  }
147
590
 
148
591
  /**
@@ -155,8 +598,7 @@ const MemoryPlugin: Plugin = {
155
598
  getAgentContext: async (ctx) => {
156
599
  const agentId = ctx.session.agentId
157
600
  if (!agentId) return null
158
-
159
- try { syncSessionArchiveMemory(ctx.session) } catch { /* archive sync is best-effort */ }
601
+ if (!shouldInjectMemoryContext(ctx.message || '')) return null
160
602
 
161
603
  const memDb = getMemoryDb()
162
604
  const memoryQuerySeed = [
@@ -178,30 +620,37 @@ const MemoryPlugin: Plugin = {
178
620
 
179
621
  const pinned = memDb.listPinned(agentId, 5)
180
622
  const pinnedLines = pinned
181
- .filter((m) => { 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
+ })
182
629
  .map(formatMemoryLine)
183
630
 
184
631
  const relevantSlice = Math.max(2, 6 - pinnedLines.length)
185
632
  const relevantLookup = memDb.searchWithLinked(memoryQuerySeed, agentId, 1, 10, 14)
186
- const relevant = relevantLookup.entries.slice(0, relevantSlice)
187
633
  const recent = memDb.list(agentId, 12).slice(0, 6)
188
- const relevantByTier = partitionMemoriesByTier(relevant)
634
+ const relevantByTier = partitionMemoriesByTier(relevantLookup.entries)
189
635
  const recentByTier = partitionMemoriesByTier(recent)
190
636
 
191
637
  const relevantLines = relevantByTier.durable
192
- .filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
193
- .map(formatMemoryLine)
194
-
195
- const archiveLines = relevantByTier.archive
196
- .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)
197
645
  .map(formatMemoryLine)
198
646
 
199
647
  const recentLines = recentByTier.durable
200
- .filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
201
- .map(formatMemoryLine)
202
-
203
- const recentArchiveLines = recentByTier.archive
204
- .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
+ })
205
654
  .map(formatMemoryLine)
206
655
 
207
656
  const parts: string[] = []
@@ -211,21 +660,15 @@ const MemoryPlugin: Plugin = {
211
660
  if (relevantLines.length) {
212
661
  parts.push(['## Relevant Memory Hits', 'These memories were retrieved by relevance for the current objective.', ...relevantLines].join('\n'))
213
662
  }
214
- if (archiveLines.length) {
215
- parts.push(['## Session Archive Hits', 'Past conversation snapshots that may restore context from older chats.', ...archiveLines].join('\n'))
216
- }
217
663
  if (recentLines.length) {
218
664
  parts.push(['## Recent Memory Notes', 'Recent durable notes that may still apply.', ...recentLines].join('\n'))
219
665
  }
220
- if (recentArchiveLines.length) {
221
- parts.push(['## Recent Session Archives', 'Recently synced conversation archives you can search instead of relying on stale live context.', ...recentArchiveLines].join('\n'))
222
- }
223
666
 
224
667
  // Memory Policy
225
668
  parts.push([
226
669
  '## My Memory',
227
- 'I have long-term memory that persists across conversations. I use it naturally I don\'t wait to be asked to remember things.',
228
- '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.',
229
672
  '',
230
673
  '**Things worth remembering:**',
231
674
  '- What the user likes, dislikes, or has corrected me on',
@@ -243,8 +686,8 @@ const MemoryPlugin: Plugin = {
243
686
  '',
244
687
  '**Good habits:**',
245
688
  '- Give memories clear titles ("User prefers dark mode" not "Note 1")',
246
- '- Use categories: preference, fact, learning, project, identity, decision',
247
- '- 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',
248
691
  '- Check what I already know before storing something new',
249
692
  '- When I learn something that corrects old knowledge, update or remove the old memory',
250
693
  ].join('\n'))
@@ -281,15 +724,14 @@ const MemoryPlugin: Plugin = {
281
724
  } catch { /* breadcrumbs are best-effort */ }
282
725
  },
283
726
  afterChatTurn: (ctx) => {
284
- if (ctx.internal) return
285
- if (ctx.source !== 'chat' && ctx.source !== 'connector') return
286
727
  const agentId = ctx.session.agentId
287
728
  if (!agentId) return
288
729
  const msg = (ctx.message || '').trim()
289
730
  const resp = (ctx.response || '').trim()
290
- if (msg.length < 20 || resp.length < 40) return
291
- if (/^(ok|okay|cool|thanks|thx|got it|nice)[.! ]*$/i.test(msg)) return
292
- 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
293
735
  const now = Date.now()
294
736
  const last = typeof ctx.session.lastAutoMemoryAt === 'number' ? ctx.session.lastAutoMemoryAt : 0
295
737
  if (last > 0 && now - last < 5 * 60 * 1000) return
@@ -297,30 +739,55 @@ const MemoryPlugin: Plugin = {
297
739
  const memDb = getMemoryDb()
298
740
  const compactMessage = msg.replace(/\s+/g, ' ').slice(0, 220)
299
741
  const compactResponse = resp.replace(/\s+/g, ' ').slice(0, 700)
300
- const autoTitle = `[auto] ${compactMessage.slice(0, 90)}`
301
- const content = `source: ${ctx.source}\nuser_request: ${compactMessage}\nassistant_outcome: ${compactResponse}`
302
- 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
+ })
303
763
  ctx.session.lastAutoMemoryAt = now
304
764
  } catch { /* auto-memory is best-effort */ }
305
765
  },
306
766
  getCapabilityDescription: () => 'I have long-term memory (`memory_tool`) — I can remember things across conversations and recall them when needed.',
307
767
  getOperatingGuidance: () => [
308
- 'Memory: 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.',
309
772
  'For open goals, form a hypothesis and execute — do not keep re-asking broad questions.',
310
773
  ],
311
774
  } as PluginHooks,
312
775
  tools: [
313
776
  {
314
777
  name: 'memory_tool',
315
- description: 'Advanced long-term memory system. 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.',
316
779
  parameters: {
317
780
  type: 'object',
318
781
  properties: {
319
- action: { type: 'string', enum: ['store', 'get', 'search', 'list', 'delete'] },
782
+ action: { type: 'string', enum: ['store', 'get', 'search', 'list', 'delete', 'update', 'link', 'unlink', 'doctor'] },
783
+ id: { type: 'string' },
320
784
  key: { type: 'string' },
785
+ title: { type: 'string' },
321
786
  value: { type: 'string' },
322
787
  category: { type: 'string' },
323
788
  query: { type: 'string' },
789
+ sources: { type: 'array', items: { type: 'string', enum: ['durable', 'working', 'archive', 'all'] } },
790
+ targetIds: { type: 'array', items: { type: 'string' } },
324
791
  scope: { type: 'string', enum: ['auto', 'all', 'global', 'shared', 'agent', 'session', 'project'] },
325
792
  },
326
793
  required: ['action']