@juspay/neurolink 9.14.0 → 9.16.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 (241) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +15 -15
  3. package/dist/adapters/video/videoAnalyzer.d.ts +1 -1
  4. package/dist/adapters/video/videoAnalyzer.js +10 -8
  5. package/dist/auth/anthropicOAuth.d.ts +377 -0
  6. package/dist/auth/anthropicOAuth.js +914 -0
  7. package/dist/auth/index.d.ts +20 -0
  8. package/dist/auth/index.js +29 -0
  9. package/dist/auth/tokenStore.d.ts +225 -0
  10. package/dist/auth/tokenStore.js +521 -0
  11. package/dist/cli/commands/auth.d.ts +50 -0
  12. package/dist/cli/commands/auth.js +1115 -0
  13. package/dist/cli/commands/setup-anthropic.js +1 -14
  14. package/dist/cli/commands/setup-azure.js +1 -12
  15. package/dist/cli/commands/setup-bedrock.js +1 -9
  16. package/dist/cli/commands/setup-google-ai.js +1 -12
  17. package/dist/cli/commands/setup-openai.js +1 -14
  18. package/dist/cli/commands/workflow.d.ts +27 -0
  19. package/dist/cli/commands/workflow.js +216 -0
  20. package/dist/cli/factories/authCommandFactory.d.ts +52 -0
  21. package/dist/cli/factories/authCommandFactory.js +146 -0
  22. package/dist/cli/factories/commandFactory.d.ts +6 -0
  23. package/dist/cli/factories/commandFactory.js +171 -22
  24. package/dist/cli/index.js +0 -1
  25. package/dist/cli/parser.js +14 -2
  26. package/dist/cli/utils/maskCredential.d.ts +11 -0
  27. package/dist/cli/utils/maskCredential.js +23 -0
  28. package/dist/constants/contextWindows.js +107 -16
  29. package/dist/constants/enums.d.ts +119 -15
  30. package/dist/constants/enums.js +182 -22
  31. package/dist/constants/index.d.ts +3 -1
  32. package/dist/constants/index.js +11 -1
  33. package/dist/context/budgetChecker.js +1 -1
  34. package/dist/context/contextCompactor.js +31 -4
  35. package/dist/context/emergencyTruncation.d.ts +21 -0
  36. package/dist/context/emergencyTruncation.js +88 -0
  37. package/dist/context/errorDetection.d.ts +16 -0
  38. package/dist/context/errorDetection.js +48 -1
  39. package/dist/context/errors.d.ts +19 -0
  40. package/dist/context/errors.js +21 -0
  41. package/dist/context/stages/slidingWindowTruncator.d.ts +6 -0
  42. package/dist/context/stages/slidingWindowTruncator.js +159 -24
  43. package/dist/core/baseProvider.js +306 -200
  44. package/dist/core/conversationMemoryManager.js +104 -61
  45. package/dist/core/evaluationProviders.js +16 -33
  46. package/dist/core/factory.js +237 -164
  47. package/dist/core/modules/GenerationHandler.js +175 -116
  48. package/dist/core/modules/MessageBuilder.js +222 -170
  49. package/dist/core/modules/StreamHandler.d.ts +1 -0
  50. package/dist/core/modules/StreamHandler.js +95 -27
  51. package/dist/core/modules/TelemetryHandler.d.ts +10 -1
  52. package/dist/core/modules/TelemetryHandler.js +25 -7
  53. package/dist/core/modules/ToolsManager.js +115 -191
  54. package/dist/core/redisConversationMemoryManager.js +418 -282
  55. package/dist/factories/providerRegistry.d.ts +5 -0
  56. package/dist/factories/providerRegistry.js +20 -2
  57. package/dist/index.d.ts +3 -3
  58. package/dist/index.js +4 -2
  59. package/dist/lib/adapters/video/videoAnalyzer.d.ts +1 -1
  60. package/dist/lib/adapters/video/videoAnalyzer.js +10 -8
  61. package/dist/lib/auth/anthropicOAuth.d.ts +377 -0
  62. package/dist/lib/auth/anthropicOAuth.js +915 -0
  63. package/dist/lib/auth/index.d.ts +20 -0
  64. package/dist/lib/auth/index.js +30 -0
  65. package/dist/lib/auth/tokenStore.d.ts +225 -0
  66. package/dist/lib/auth/tokenStore.js +522 -0
  67. package/dist/lib/constants/contextWindows.js +107 -16
  68. package/dist/lib/constants/enums.d.ts +119 -15
  69. package/dist/lib/constants/enums.js +182 -22
  70. package/dist/lib/constants/index.d.ts +3 -1
  71. package/dist/lib/constants/index.js +11 -1
  72. package/dist/lib/context/budgetChecker.js +1 -1
  73. package/dist/lib/context/contextCompactor.js +31 -4
  74. package/dist/lib/context/emergencyTruncation.d.ts +21 -0
  75. package/dist/lib/context/emergencyTruncation.js +89 -0
  76. package/dist/lib/context/errorDetection.d.ts +16 -0
  77. package/dist/lib/context/errorDetection.js +48 -1
  78. package/dist/lib/context/errors.d.ts +19 -0
  79. package/dist/lib/context/errors.js +22 -0
  80. package/dist/lib/context/stages/slidingWindowTruncator.d.ts +6 -0
  81. package/dist/lib/context/stages/slidingWindowTruncator.js +159 -24
  82. package/dist/lib/core/baseProvider.js +306 -200
  83. package/dist/lib/core/conversationMemoryManager.js +104 -61
  84. package/dist/lib/core/evaluationProviders.js +16 -33
  85. package/dist/lib/core/factory.js +237 -164
  86. package/dist/lib/core/modules/GenerationHandler.js +175 -116
  87. package/dist/lib/core/modules/MessageBuilder.js +222 -170
  88. package/dist/lib/core/modules/StreamHandler.d.ts +1 -0
  89. package/dist/lib/core/modules/StreamHandler.js +95 -27
  90. package/dist/lib/core/modules/TelemetryHandler.d.ts +10 -1
  91. package/dist/lib/core/modules/TelemetryHandler.js +25 -7
  92. package/dist/lib/core/modules/ToolsManager.js +115 -191
  93. package/dist/lib/core/redisConversationMemoryManager.js +418 -282
  94. package/dist/lib/factories/providerRegistry.d.ts +5 -0
  95. package/dist/lib/factories/providerRegistry.js +20 -2
  96. package/dist/lib/index.d.ts +3 -3
  97. package/dist/lib/index.js +4 -2
  98. package/dist/lib/mcp/externalServerManager.js +66 -0
  99. package/dist/lib/mcp/mcpCircuitBreaker.js +24 -0
  100. package/dist/lib/mcp/mcpClientFactory.js +16 -0
  101. package/dist/lib/mcp/toolDiscoveryService.js +32 -6
  102. package/dist/lib/mcp/toolRegistry.js +193 -123
  103. package/dist/lib/models/anthropicModels.d.ts +267 -0
  104. package/dist/lib/models/anthropicModels.js +528 -0
  105. package/dist/lib/neurolink.d.ts +6 -0
  106. package/dist/lib/neurolink.js +1162 -646
  107. package/dist/lib/providers/amazonBedrock.d.ts +1 -1
  108. package/dist/lib/providers/amazonBedrock.js +521 -319
  109. package/dist/lib/providers/anthropic.d.ts +123 -2
  110. package/dist/lib/providers/anthropic.js +873 -27
  111. package/dist/lib/providers/anthropicBaseProvider.js +77 -17
  112. package/dist/lib/providers/googleAiStudio.d.ts +1 -1
  113. package/dist/lib/providers/googleAiStudio.js +292 -227
  114. package/dist/lib/providers/googleVertex.d.ts +36 -1
  115. package/dist/lib/providers/googleVertex.js +553 -260
  116. package/dist/lib/providers/ollama.js +329 -278
  117. package/dist/lib/providers/openAI.js +77 -19
  118. package/dist/lib/providers/sagemaker/parsers.js +3 -3
  119. package/dist/lib/providers/sagemaker/streaming.js +3 -3
  120. package/dist/lib/proxy/proxyFetch.js +81 -48
  121. package/dist/lib/rag/ChunkerFactory.js +1 -1
  122. package/dist/lib/rag/chunkers/MarkdownChunker.d.ts +22 -0
  123. package/dist/lib/rag/chunkers/MarkdownChunker.js +213 -9
  124. package/dist/lib/rag/chunking/markdownChunker.d.ts +16 -0
  125. package/dist/lib/rag/chunking/markdownChunker.js +174 -2
  126. package/dist/lib/rag/pipeline/contextAssembly.js +2 -1
  127. package/dist/lib/rag/ragIntegration.d.ts +18 -1
  128. package/dist/lib/rag/ragIntegration.js +94 -14
  129. package/dist/lib/rag/retrieval/vectorQueryTool.js +21 -4
  130. package/dist/lib/server/abstract/baseServerAdapter.js +4 -1
  131. package/dist/lib/server/adapters/fastifyAdapter.js +35 -30
  132. package/dist/lib/services/server/ai/observability/instrumentation.d.ts +32 -0
  133. package/dist/lib/services/server/ai/observability/instrumentation.js +39 -0
  134. package/dist/lib/telemetry/attributes.d.ts +52 -0
  135. package/dist/lib/telemetry/attributes.js +61 -0
  136. package/dist/lib/telemetry/index.d.ts +3 -0
  137. package/dist/lib/telemetry/index.js +3 -0
  138. package/dist/lib/telemetry/telemetryService.d.ts +6 -0
  139. package/dist/lib/telemetry/telemetryService.js +6 -0
  140. package/dist/lib/telemetry/tracers.d.ts +15 -0
  141. package/dist/lib/telemetry/tracers.js +17 -0
  142. package/dist/lib/telemetry/withSpan.d.ts +9 -0
  143. package/dist/lib/telemetry/withSpan.js +35 -0
  144. package/dist/lib/types/contextTypes.d.ts +10 -0
  145. package/dist/lib/types/errors.d.ts +62 -0
  146. package/dist/lib/types/errors.js +107 -0
  147. package/dist/lib/types/index.d.ts +2 -1
  148. package/dist/lib/types/index.js +2 -0
  149. package/dist/lib/types/providers.d.ts +107 -0
  150. package/dist/lib/types/providers.js +69 -0
  151. package/dist/lib/types/streamTypes.d.ts +14 -0
  152. package/dist/lib/types/subscriptionTypes.d.ts +893 -0
  153. package/dist/lib/types/subscriptionTypes.js +8 -0
  154. package/dist/lib/utils/conversationMemory.js +121 -82
  155. package/dist/lib/utils/logger.d.ts +5 -0
  156. package/dist/lib/utils/logger.js +50 -2
  157. package/dist/lib/utils/messageBuilder.js +22 -42
  158. package/dist/lib/utils/modelDetection.js +3 -3
  159. package/dist/lib/utils/providerConfig.d.ts +167 -0
  160. package/dist/lib/utils/providerConfig.js +619 -9
  161. package/dist/lib/utils/providerRetry.d.ts +41 -0
  162. package/dist/lib/utils/providerRetry.js +114 -0
  163. package/dist/lib/utils/retryability.d.ts +14 -0
  164. package/dist/lib/utils/retryability.js +23 -0
  165. package/dist/lib/utils/sanitizers/svg.js +4 -5
  166. package/dist/lib/utils/tokenEstimation.d.ts +11 -1
  167. package/dist/lib/utils/tokenEstimation.js +19 -4
  168. package/dist/lib/utils/videoAnalysisProcessor.js +7 -3
  169. package/dist/mcp/externalServerManager.js +66 -0
  170. package/dist/mcp/mcpCircuitBreaker.js +24 -0
  171. package/dist/mcp/mcpClientFactory.js +16 -0
  172. package/dist/mcp/toolDiscoveryService.js +32 -6
  173. package/dist/mcp/toolRegistry.js +193 -123
  174. package/dist/models/anthropicModels.d.ts +267 -0
  175. package/dist/models/anthropicModels.js +527 -0
  176. package/dist/neurolink.d.ts +6 -0
  177. package/dist/neurolink.js +1162 -646
  178. package/dist/providers/amazonBedrock.d.ts +1 -1
  179. package/dist/providers/amazonBedrock.js +521 -319
  180. package/dist/providers/anthropic.d.ts +123 -2
  181. package/dist/providers/anthropic.js +873 -27
  182. package/dist/providers/anthropicBaseProvider.js +77 -17
  183. package/dist/providers/googleAiStudio.d.ts +1 -1
  184. package/dist/providers/googleAiStudio.js +292 -227
  185. package/dist/providers/googleVertex.d.ts +36 -1
  186. package/dist/providers/googleVertex.js +553 -260
  187. package/dist/providers/ollama.js +329 -278
  188. package/dist/providers/openAI.js +77 -19
  189. package/dist/providers/sagemaker/parsers.js +3 -3
  190. package/dist/providers/sagemaker/streaming.js +3 -3
  191. package/dist/proxy/proxyFetch.js +81 -48
  192. package/dist/rag/ChunkerFactory.js +1 -1
  193. package/dist/rag/chunkers/MarkdownChunker.d.ts +22 -0
  194. package/dist/rag/chunkers/MarkdownChunker.js +213 -9
  195. package/dist/rag/chunking/markdownChunker.d.ts +16 -0
  196. package/dist/rag/chunking/markdownChunker.js +174 -2
  197. package/dist/rag/pipeline/contextAssembly.js +2 -1
  198. package/dist/rag/ragIntegration.d.ts +18 -1
  199. package/dist/rag/ragIntegration.js +94 -14
  200. package/dist/rag/retrieval/vectorQueryTool.js +21 -4
  201. package/dist/server/abstract/baseServerAdapter.js +4 -1
  202. package/dist/server/adapters/fastifyAdapter.js +35 -30
  203. package/dist/services/server/ai/observability/instrumentation.d.ts +32 -0
  204. package/dist/services/server/ai/observability/instrumentation.js +39 -0
  205. package/dist/telemetry/attributes.d.ts +52 -0
  206. package/dist/telemetry/attributes.js +60 -0
  207. package/dist/telemetry/index.d.ts +3 -0
  208. package/dist/telemetry/index.js +3 -0
  209. package/dist/telemetry/telemetryService.d.ts +6 -0
  210. package/dist/telemetry/telemetryService.js +6 -0
  211. package/dist/telemetry/tracers.d.ts +15 -0
  212. package/dist/telemetry/tracers.js +16 -0
  213. package/dist/telemetry/withSpan.d.ts +9 -0
  214. package/dist/telemetry/withSpan.js +34 -0
  215. package/dist/types/contextTypes.d.ts +10 -0
  216. package/dist/types/errors.d.ts +62 -0
  217. package/dist/types/errors.js +107 -0
  218. package/dist/types/index.d.ts +2 -1
  219. package/dist/types/index.js +2 -0
  220. package/dist/types/providers.d.ts +107 -0
  221. package/dist/types/providers.js +69 -0
  222. package/dist/types/streamTypes.d.ts +14 -0
  223. package/dist/types/subscriptionTypes.d.ts +893 -0
  224. package/dist/types/subscriptionTypes.js +7 -0
  225. package/dist/utils/conversationMemory.js +121 -82
  226. package/dist/utils/logger.d.ts +5 -0
  227. package/dist/utils/logger.js +50 -2
  228. package/dist/utils/messageBuilder.js +22 -42
  229. package/dist/utils/modelDetection.js +3 -3
  230. package/dist/utils/providerConfig.d.ts +167 -0
  231. package/dist/utils/providerConfig.js +619 -9
  232. package/dist/utils/providerRetry.d.ts +41 -0
  233. package/dist/utils/providerRetry.js +113 -0
  234. package/dist/utils/retryability.d.ts +14 -0
  235. package/dist/utils/retryability.js +22 -0
  236. package/dist/utils/sanitizers/svg.js +4 -5
  237. package/dist/utils/tokenEstimation.d.ts +11 -1
  238. package/dist/utils/tokenEstimation.js +19 -4
  239. package/dist/utils/videoAnalysisProcessor.js +7 -3
  240. package/dist/workflow/config.d.ts +26 -26
  241. package/package.json +2 -1
@@ -2,6 +2,8 @@
2
2
  * Redis Conversation Memory Manager for NeuroLink
3
3
  * Redis-based implementation of conversation storage with same interface as ConversationMemoryManager
4
4
  */
5
+ import { SpanKind, SpanStatusCode } from "@opentelemetry/api";
6
+ import { tracers } from "../telemetry/tracers.js";
5
7
  import { randomUUID } from "crypto";
6
8
  import { MESSAGES_PER_TURN } from "../config/conversationMemory.js";
7
9
  import { generateToolOutputPreview } from "../context/toolOutputLimits.js";
@@ -9,8 +11,10 @@ import { SummarizationEngine } from "../context/summarizationEngine.js";
9
11
  import { NeuroLink } from "../neurolink.js";
10
12
  import { ConversationMemoryError } from "../types/conversation.js";
11
13
  import { buildContextFromPointer, getEffectiveTokenThreshold, } from "../utils/conversationMemory.js";
14
+ import { runWithCurrentLangfuseContext } from "../services/server/ai/observability/instrumentation.js";
12
15
  import { logger } from "../utils/logger.js";
13
16
  import { createRedisClient, deserializeConversation, getNormalizedConfig, getPooledRedisClient, getSessionKey, getUserSessionsKey, releasePooledRedisClient, scanKeys, serializeConversation, } from "../utils/redis.js";
17
+ const redisTracer = tracers.redis;
14
18
  /**
15
19
  * Redis-based implementation of the ConversationMemoryManager
16
20
  * Uses the same interface but stores data in Redis
@@ -48,40 +52,57 @@ export class RedisConversationMemoryManager {
48
52
  logger.debug("[RedisConversationMemoryManager] Already initialized, skipping");
49
53
  return;
50
54
  }
51
- try {
52
- logger.debug("[RedisConversationMemoryManager] Initializing with config", {
53
- host: this.redisConfig.host,
54
- port: this.redisConfig.port,
55
- keyPrefix: this.redisConfig.keyPrefix,
56
- ttl: this.redisConfig.ttl,
57
- });
58
- this.redisClient = await getPooledRedisClient(this.redisConfig);
59
- this.isInitialized = true;
60
- logger.info("RedisConversationMemoryManager initialized", {
61
- storage: "redis",
62
- host: this.redisConfig.host,
63
- port: this.redisConfig.port,
64
- maxSessions: this.config.maxSessions,
65
- maxTurnsPerSession: this.config.maxTurnsPerSession,
66
- });
67
- logger.debug("[RedisConversationMemoryManager] Redis client created successfully", {
68
- clientType: this.redisClient?.constructor?.name || "unknown",
69
- isConnected: !!this.redisClient,
70
- });
71
- }
72
- catch (error) {
73
- logger.error("[RedisConversationMemoryManager] Failed to initialize", {
74
- error: error instanceof Error ? error.message : String(error),
75
- stack: error instanceof Error ? error.stack : undefined,
76
- config: {
55
+ await redisTracer.startActiveSpan("neurolink.memory.initialize", {
56
+ kind: SpanKind.CLIENT,
57
+ attributes: {
58
+ "redis.host": this.redisConfig.host,
59
+ "redis.port": this.redisConfig.port,
60
+ "redis.key_prefix": this.redisConfig.keyPrefix,
61
+ },
62
+ }, async (span) => {
63
+ try {
64
+ logger.debug("[RedisConversationMemoryManager] Initializing with config", {
77
65
  host: this.redisConfig.host,
78
66
  port: this.redisConfig.port,
79
- },
80
- });
81
- throw new ConversationMemoryError("Failed to initialize Redis conversation memory", "CONFIG_ERROR", {
82
- error: error instanceof Error ? error.message : String(error),
83
- });
84
- }
67
+ keyPrefix: this.redisConfig.keyPrefix,
68
+ ttl: this.redisConfig.ttl,
69
+ });
70
+ this.redisClient = await getPooledRedisClient(this.redisConfig);
71
+ this.isInitialized = true;
72
+ logger.info("RedisConversationMemoryManager initialized", {
73
+ storage: "redis",
74
+ host: this.redisConfig.host,
75
+ port: this.redisConfig.port,
76
+ maxSessions: this.config.maxSessions,
77
+ maxTurnsPerSession: this.config.maxTurnsPerSession,
78
+ });
79
+ logger.debug("[RedisConversationMemoryManager] Redis client created successfully", {
80
+ clientType: this.redisClient?.constructor?.name || "unknown",
81
+ isConnected: !!this.redisClient,
82
+ });
83
+ }
84
+ catch (error) {
85
+ span.setStatus({
86
+ code: SpanStatusCode.ERROR,
87
+ message: error instanceof Error ? error.message : String(error),
88
+ });
89
+ span.recordException(error instanceof Error ? error : new Error(String(error)));
90
+ logger.error("[RedisConversationMemoryManager] Failed to initialize", {
91
+ error: error instanceof Error ? error.message : String(error),
92
+ stack: error instanceof Error ? error.stack : undefined,
93
+ config: {
94
+ host: this.redisConfig.host,
95
+ port: this.redisConfig.port,
96
+ },
97
+ });
98
+ throw new ConversationMemoryError("Failed to initialize Redis conversation memory", "CONFIG_ERROR", {
99
+ error: error instanceof Error ? error.message : String(error),
100
+ });
101
+ }
102
+ finally {
103
+ span.end();
104
+ }
105
+ });
85
106
  }
86
107
  /** Whether this memory manager can persist data (Redis connected and initialized) */
87
108
  get canPersist() {
@@ -108,63 +129,80 @@ export class RedisConversationMemoryManager {
108
129
  if (!this.redisClient) {
109
130
  return undefined;
110
131
  }
111
- try {
112
- const redisKey = getSessionKey(this.redisConfig, sessionId, userId);
113
- const conversationData = await this.redisClient.get(redisKey);
114
- const conversation = deserializeConversation(conversationData || null);
115
- if (!conversation) {
116
- return undefined;
132
+ return redisTracer.startActiveSpan("neurolink.memory.getSession", { kind: SpanKind.CLIENT, attributes: { "session.id": sessionId } }, async (span) => {
133
+ if (userId) {
134
+ span.setAttribute("user.id", userId);
117
135
  }
118
- // Log session load metadata for observability
119
- const blobSizeBytes = conversationData
120
- ? Buffer.byteLength(conversationData, "utf8")
121
- : 0;
122
- const messageCount = conversation.messages.length;
123
- const hasSummary = !!conversation.summarizedUpToMessageId;
124
- const pointerIndex = hasSummary
125
- ? conversation.messages.findIndex((msg) => msg.id === conversation.summarizedUpToMessageId)
126
- : -1;
127
- const recentMessageCount = hasSummary && pointerIndex !== -1
128
- ? messageCount - pointerIndex - 1
129
- : messageCount;
130
- logger.info("[ConversationMemory] Session loaded", {
131
- requestId,
132
- sessionId,
133
- blobSizeBytes,
134
- messageCount,
135
- hasSummary,
136
- recentMessageCount,
137
- });
138
- if (blobSizeBytes > 512 * 1024) {
139
- logger.warn("[ConversationMemory] Large session blob", {
136
+ try {
137
+ const redisKey = getSessionKey(this.redisConfig, sessionId, userId);
138
+ const conversationData = await this.redisClient.get(redisKey);
139
+ const conversation = deserializeConversation(conversationData || null);
140
+ if (!conversation) {
141
+ span.setAttribute("session.found", false);
142
+ return undefined;
143
+ }
144
+ span.setAttribute("session.found", true);
145
+ // Log session load metadata for observability
146
+ const blobSizeBytes = conversationData
147
+ ? Buffer.byteLength(conversationData, "utf8")
148
+ : 0;
149
+ const messageCount = conversation.messages.length;
150
+ const hasSummary = !!conversation.summarizedUpToMessageId;
151
+ const pointerIndex = hasSummary
152
+ ? conversation.messages.findIndex((msg) => msg.id === conversation.summarizedUpToMessageId)
153
+ : -1;
154
+ const recentMessageCount = hasSummary && pointerIndex !== -1
155
+ ? messageCount - pointerIndex - 1
156
+ : messageCount;
157
+ span.setAttribute("message.count", messageCount);
158
+ span.setAttribute("blob.size_bytes", blobSizeBytes);
159
+ logger.info("[ConversationMemory] Session loaded", {
140
160
  requestId,
141
161
  sessionId,
142
162
  blobSizeBytes,
143
163
  messageCount,
164
+ hasSummary,
165
+ recentMessageCount,
144
166
  });
167
+ if (blobSizeBytes > 512 * 1024) {
168
+ logger.warn("[ConversationMemory] Large session blob", {
169
+ requestId,
170
+ sessionId,
171
+ blobSizeBytes,
172
+ messageCount,
173
+ });
174
+ }
175
+ return {
176
+ sessionId: conversation.sessionId,
177
+ userId: conversation.userId,
178
+ messages: conversation.messages,
179
+ summarizedUpToMessageId: conversation.summarizedUpToMessageId,
180
+ summarizedMessage: conversation.summarizedMessage,
181
+ tokenThreshold: conversation.tokenThreshold,
182
+ lastTokenCount: conversation.lastTokenCount,
183
+ lastCountedAt: conversation.lastCountedAt,
184
+ lastApiTokenCount: conversation.lastApiTokenCount,
185
+ createdAt: new Date(conversation.createdAt).getTime(),
186
+ lastActivity: new Date(conversation.updatedAt).getTime(),
187
+ };
145
188
  }
146
- return {
147
- sessionId: conversation.sessionId,
148
- userId: conversation.userId,
149
- messages: conversation.messages,
150
- summarizedUpToMessageId: conversation.summarizedUpToMessageId,
151
- summarizedMessage: conversation.summarizedMessage,
152
- tokenThreshold: conversation.tokenThreshold,
153
- lastTokenCount: conversation.lastTokenCount,
154
- lastCountedAt: conversation.lastCountedAt,
155
- lastApiTokenCount: conversation.lastApiTokenCount,
156
- createdAt: new Date(conversation.createdAt).getTime(),
157
- lastActivity: new Date(conversation.updatedAt).getTime(),
158
- };
159
- }
160
- catch (error) {
161
- logger.error("[RedisConversationMemoryManager] Failed to get session", {
162
- sessionId,
163
- userId,
164
- error: error instanceof Error ? error.message : String(error),
165
- });
166
- return undefined;
167
- }
189
+ catch (error) {
190
+ span.setStatus({
191
+ code: SpanStatusCode.ERROR,
192
+ message: error instanceof Error ? error.message : String(error),
193
+ });
194
+ span.recordException(error instanceof Error ? error : new Error(String(error)));
195
+ logger.error("[RedisConversationMemoryManager] Failed to get session", {
196
+ sessionId,
197
+ userId,
198
+ error: error instanceof Error ? error.message : String(error),
199
+ });
200
+ return undefined;
201
+ }
202
+ finally {
203
+ span.end();
204
+ }
205
+ });
168
206
  }
169
207
  /**
170
208
  * Get raw session data without any filtering or transformation.
@@ -335,156 +373,183 @@ export class RedisConversationMemoryManager {
335
373
  userId: options.userId,
336
374
  });
337
375
  await this.ensureInitialized();
338
- try {
339
- if (!this.redisClient) {
340
- throw new Error("Redis client not initialized");
341
- }
342
- const redisKey = getSessionKey(this.redisConfig, options.sessionId, options.userId);
343
- const conversationData = await this.redisClient.get(redisKey);
344
- let conversation = deserializeConversation(conversationData);
345
- const currentTime = new Date().toISOString();
346
- const normalizedUserId = options.userId || "randomUser";
347
- if (!conversation) {
348
- const titleGenerationKey = `${options.sessionId}:${normalizedUserId}`;
349
- setImmediate(async () => {
350
- if (this.titleGenerationInProgress.has(titleGenerationKey)) {
351
- return;
352
- }
353
- this.titleGenerationInProgress.add(titleGenerationKey);
354
- try {
355
- const title = await this.generateConversationTitle(options.userMessage);
356
- const updatedRedisKey = getSessionKey(this.redisConfig, options.sessionId, options.userId || undefined);
357
- const updatedConversationData = await this.redisClient?.get(updatedRedisKey);
358
- const updatedConversation = deserializeConversation(updatedConversationData || null);
359
- if (updatedConversation) {
360
- updatedConversation.title = title;
361
- updatedConversation.updatedAt = new Date().toISOString();
362
- const serializedData = serializeConversation(updatedConversation);
363
- await this.redisClient?.set(updatedRedisKey, serializedData);
364
- if (this.redisConfig.ttl > 0) {
365
- await this.redisClient?.expire(updatedRedisKey, this.redisConfig.ttl);
366
- }
367
- }
368
- }
369
- catch (titleError) {
370
- logger.warn("[RedisConversationMemoryManager] Failed to generate conversation title in background", {
371
- sessionId: options.sessionId,
372
- userId: normalizedUserId,
373
- error: titleError instanceof Error
374
- ? titleError.message
375
- : String(titleError),
376
- });
377
- }
378
- finally {
379
- this.titleGenerationInProgress.delete(titleGenerationKey);
380
- }
381
- });
382
- conversation = {
383
- id: randomUUID(),
384
- title: "New Conversation", // Temporary title until generated
385
- sessionId: options.sessionId,
386
- userId: normalizedUserId,
387
- createdAt: options.startTimeStamp?.toISOString() || currentTime,
388
- updatedAt: options.startTimeStamp?.toISOString() || currentTime,
389
- messages: [],
390
- };
391
- }
392
- else {
393
- conversation.updatedAt = currentTime;
394
- }
395
- const tokenThreshold = options.providerDetails
396
- ? getEffectiveTokenThreshold(options.providerDetails.provider, options.providerDetails.model, this.config.tokenThreshold, conversation.tokenThreshold)
397
- : this.config.tokenThreshold || 50000;
398
- const userMsg = {
399
- id: randomUUID(),
400
- timestamp: options.startTimeStamp?.toISOString() || this.generateTimestamp(),
401
- role: "user",
402
- content: options.userMessage,
403
- };
404
- conversation.messages.push(userMsg);
405
- await this.flushPendingToolData(conversation, options.sessionId, normalizedUserId);
406
- const assistantMsg = {
407
- id: randomUUID(),
408
- timestamp: this.generateTimestamp(),
409
- role: "assistant",
410
- content: options.aiResponse,
411
- events: options.events || undefined,
412
- };
413
- conversation.messages.push(assistantMsg);
414
- // Store API-reported token counts if available
415
- if (options.tokenUsage) {
416
- conversation.lastApiTokenCount = options.tokenUsage;
417
- }
418
- logger.info("[RedisConversationMemoryManager] Added new messages", {
419
- sessionId: conversation.sessionId,
420
- userId: conversation.userId,
421
- });
422
- // Use per-request enableSummarization with higher priority than instance config
423
- const shouldSummarize = options.enableSummarization !== undefined
424
- ? options.enableSummarization
425
- : this.config.enableSummarization;
426
- if (shouldSummarize) {
376
+ // NLK-GAP-012: Add span for storeTurn CRUD operation
377
+ return redisTracer.startActiveSpan("neurolink.memory.storeTurn", {
378
+ kind: SpanKind.CLIENT,
379
+ attributes: {
380
+ "session.id": options.sessionId,
381
+ ...(options.userId && { "user.id": options.userId }),
382
+ },
383
+ }, async (span) => {
384
+ try {
385
+ if (!this.redisClient) {
386
+ throw new Error("Redis client not initialized");
387
+ }
388
+ const redisKey = getSessionKey(this.redisConfig, options.sessionId, options.userId);
389
+ const conversationData = await this.redisClient.get(redisKey);
390
+ let conversation = deserializeConversation(conversationData);
391
+ const currentTime = new Date().toISOString();
427
392
  const normalizedUserId = options.userId || "randomUser";
428
- const summarizationKey = `${options.sessionId}:${normalizedUserId}`;
429
- // Only trigger summarization if not already in progress for this session
430
- if (!this.summarizationInProgress.has(summarizationKey)) {
431
- setImmediate(async () => {
393
+ if (!conversation) {
394
+ const titleGenerationKey = `${options.sessionId}:${normalizedUserId}`;
395
+ // Capture the current Langfuse ALS context before setImmediate,
396
+ // which breaks automatic AsyncLocalStorage propagation and would
397
+ // otherwise cause orphaned traces in Langfuse.
398
+ const generateTitleWithContext = runWithCurrentLangfuseContext(async () => {
399
+ if (this.titleGenerationInProgress.has(titleGenerationKey)) {
400
+ return;
401
+ }
402
+ this.titleGenerationInProgress.add(titleGenerationKey);
432
403
  try {
433
- await this.checkAndSummarize(conversation, tokenThreshold, options.sessionId, options.userId, options.requestId);
404
+ const title = await this.generateConversationTitle(options.userMessage);
405
+ const updatedRedisKey = getSessionKey(this.redisConfig, options.sessionId, options.userId || undefined);
406
+ const updatedConversationData = await this.redisClient?.get(updatedRedisKey);
407
+ const updatedConversation = deserializeConversation(updatedConversationData || null);
408
+ if (updatedConversation) {
409
+ updatedConversation.title = title;
410
+ updatedConversation.updatedAt = new Date().toISOString();
411
+ const serializedData = serializeConversation(updatedConversation);
412
+ await this.redisClient?.set(updatedRedisKey, serializedData);
413
+ if (this.redisConfig.ttl > 0) {
414
+ await this.redisClient?.expire(updatedRedisKey, this.redisConfig.ttl);
415
+ }
416
+ }
434
417
  }
435
- catch (error) {
436
- logger.error("Background summarization failed", {
437
- sessionId: conversation.sessionId,
438
- error: error instanceof Error ? error.message : String(error),
418
+ catch (titleError) {
419
+ logger.warn("[RedisConversationMemoryManager] Failed to generate conversation title in background", {
420
+ sessionId: options.sessionId,
421
+ userId: normalizedUserId,
422
+ error: titleError instanceof Error
423
+ ? titleError.message
424
+ : String(titleError),
439
425
  });
440
426
  }
427
+ finally {
428
+ this.titleGenerationInProgress.delete(titleGenerationKey);
429
+ }
441
430
  });
442
- }
443
- else {
444
- logger.debug("[RedisConversationMemoryManager] Summarization already in progress, skipping", {
431
+ setImmediate(generateTitleWithContext);
432
+ conversation = {
433
+ id: randomUUID(),
434
+ title: "New Conversation", // Temporary title until generated
445
435
  sessionId: options.sessionId,
446
436
  userId: normalizedUserId,
447
- });
437
+ createdAt: options.startTimeStamp?.toISOString() || currentTime,
438
+ updatedAt: options.startTimeStamp?.toISOString() || currentTime,
439
+ messages: [],
440
+ };
448
441
  }
449
- }
450
- const serializedData = serializeConversation(conversation);
451
- await this.redisClient.set(redisKey, serializedData);
452
- // Log turn storage metadata for observability
453
- const blobSizeBytes = Buffer.byteLength(serializedData, "utf8");
454
- logger.info("[ConversationMemory] Turn stored", {
455
- requestId: options.requestId,
456
- sessionId: options.sessionId,
457
- blobSizeBytes,
458
- totalMessages: conversation.messages.length,
459
- userMsgChars: options.userMessage.length,
460
- assistantMsgChars: options.aiResponse.length,
461
- });
462
- if (blobSizeBytes > 512 * 1024) {
463
- logger.warn("[ConversationMemory] Large session blob", {
442
+ else {
443
+ conversation.updatedAt = currentTime;
444
+ }
445
+ const tokenThreshold = options.providerDetails
446
+ ? getEffectiveTokenThreshold(options.providerDetails.provider, options.providerDetails.model, this.config.tokenThreshold, conversation.tokenThreshold)
447
+ : this.config.tokenThreshold || 50000;
448
+ const userMsg = {
449
+ id: randomUUID(),
450
+ timestamp: options.startTimeStamp?.toISOString() || this.generateTimestamp(),
451
+ role: "user",
452
+ content: options.userMessage,
453
+ };
454
+ conversation.messages.push(userMsg);
455
+ await this.flushPendingToolData(conversation, options.sessionId, normalizedUserId);
456
+ const assistantMsg = {
457
+ id: randomUUID(),
458
+ timestamp: this.generateTimestamp(),
459
+ role: "assistant",
460
+ content: options.aiResponse,
461
+ events: options.events || undefined,
462
+ };
463
+ conversation.messages.push(assistantMsg);
464
+ // Store API-reported token counts if available
465
+ if (options.tokenUsage) {
466
+ conversation.lastApiTokenCount = options.tokenUsage;
467
+ }
468
+ logger.info("[RedisConversationMemoryManager] Added new messages", {
469
+ sessionId: conversation.sessionId,
470
+ userId: conversation.userId,
471
+ });
472
+ // Use per-request enableSummarization with higher priority than instance config
473
+ const shouldSummarize = options.enableSummarization !== undefined
474
+ ? options.enableSummarization
475
+ : this.config.enableSummarization;
476
+ if (shouldSummarize) {
477
+ const normalizedUserId = options.userId || "randomUser";
478
+ const summarizationKey = `${options.sessionId}:${normalizedUserId}`;
479
+ // Only trigger summarization if not already in progress for this session
480
+ if (!this.summarizationInProgress.has(summarizationKey)) {
481
+ // Capture the current Langfuse ALS context before setImmediate,
482
+ // which breaks automatic AsyncLocalStorage propagation and would
483
+ // otherwise cause orphaned traces in Langfuse.
484
+ const summarizeWithContext = runWithCurrentLangfuseContext(async () => {
485
+ try {
486
+ await this.checkAndSummarize(conversation, tokenThreshold, options.sessionId, options.userId, options.requestId);
487
+ }
488
+ catch (error) {
489
+ logger.error("Background summarization failed", {
490
+ sessionId: conversation.sessionId,
491
+ error: error instanceof Error ? error.message : String(error),
492
+ });
493
+ }
494
+ });
495
+ setImmediate(summarizeWithContext);
496
+ }
497
+ else {
498
+ logger.debug("[RedisConversationMemoryManager] Summarization already in progress, skipping", {
499
+ sessionId: options.sessionId,
500
+ userId: normalizedUserId,
501
+ });
502
+ }
503
+ }
504
+ const serializedData = serializeConversation(conversation);
505
+ await this.redisClient.set(redisKey, serializedData);
506
+ // Log turn storage metadata for observability
507
+ const blobSizeBytes = Buffer.byteLength(serializedData, "utf8");
508
+ logger.info("[ConversationMemory] Turn stored", {
464
509
  requestId: options.requestId,
465
510
  sessionId: options.sessionId,
466
511
  blobSizeBytes,
467
- messageCount: conversation.messages.length,
512
+ totalMessages: conversation.messages.length,
513
+ userMsgChars: options.userMessage.length,
514
+ assistantMsgChars: options.aiResponse.length,
515
+ });
516
+ if (blobSizeBytes > 512 * 1024) {
517
+ logger.warn("[ConversationMemory] Large session blob", {
518
+ requestId: options.requestId,
519
+ sessionId: options.sessionId,
520
+ blobSizeBytes,
521
+ messageCount: conversation.messages.length,
522
+ });
523
+ }
524
+ if (this.redisConfig.ttl > 0) {
525
+ await this.redisClient.expire(redisKey, this.redisConfig.ttl);
526
+ }
527
+ if (options.userId) {
528
+ await this.addUserSession(options.userId, options.sessionId);
529
+ }
530
+ span.setAttribute("message.count", conversation.messages.length);
531
+ span.setStatus({ code: SpanStatusCode.OK });
532
+ logger.debug("[RedisConversationMemoryManager] Successfully stored conversation turn", {
533
+ sessionId: options.sessionId,
534
+ totalMessages: conversation.messages.length,
535
+ title: conversation.title,
468
536
  });
469
537
  }
470
- if (this.redisConfig.ttl > 0) {
471
- await this.redisClient.expire(redisKey, this.redisConfig.ttl);
538
+ catch (error) {
539
+ span.setStatus({
540
+ code: SpanStatusCode.ERROR,
541
+ message: error instanceof Error ? error.message : String(error),
542
+ });
543
+ span.recordException(error instanceof Error ? error : new Error(String(error)));
544
+ throw new ConversationMemoryError(`Failed to store conversation turn in Redis for session ${options.sessionId}`, "STORAGE_ERROR", {
545
+ sessionId: options.sessionId,
546
+ error: error instanceof Error ? error.message : String(error),
547
+ });
472
548
  }
473
- if (options.userId) {
474
- await this.addUserSession(options.userId, options.sessionId);
549
+ finally {
550
+ span.end();
475
551
  }
476
- logger.debug("[RedisConversationMemoryManager] Successfully stored conversation turn", {
477
- sessionId: options.sessionId,
478
- totalMessages: conversation.messages.length,
479
- title: conversation.title,
480
- });
481
- }
482
- catch (error) {
483
- throw new ConversationMemoryError(`Failed to store conversation turn in Redis for session ${options.sessionId}`, "STORAGE_ERROR", {
484
- sessionId: options.sessionId,
485
- error: error instanceof Error ? error.message : String(error),
486
- });
487
- }
552
+ });
488
553
  }
489
554
  /**
490
555
  * Check if summarization is needed based on token count
@@ -521,10 +586,24 @@ export class RedisConversationMemoryManager {
521
586
  conversation.summarizedMessage = session.summarizedMessage;
522
587
  if (this.redisClient) {
523
588
  const redisKey = getSessionKey(this.redisConfig, sessionId, userId);
524
- const serializedData = serializeConversation(conversation);
525
- await this.redisClient.set(redisKey, serializedData);
526
- if (this.redisConfig.ttl > 0) {
527
- await this.redisClient.expire(redisKey, this.redisConfig.ttl);
589
+ // Re-read current state to avoid clobbering messages added during summarization
590
+ const latestData = await this.redisClient.get(redisKey);
591
+ if (latestData) {
592
+ const latestConversation = deserializeConversation(latestData);
593
+ if (latestConversation) {
594
+ // Apply only summarization metadata onto the fresh state
595
+ latestConversation.summarizedUpToMessageId =
596
+ conversation.summarizedUpToMessageId;
597
+ latestConversation.summarizedMessage =
598
+ conversation.summarizedMessage;
599
+ latestConversation.lastTokenCount = conversation.lastTokenCount;
600
+ latestConversation.lastCountedAt = conversation.lastCountedAt;
601
+ const freshSerialized = serializeConversation(latestConversation);
602
+ await this.redisClient.set(redisKey, freshSerialized);
603
+ if (this.redisConfig.ttl > 0) {
604
+ await this.redisClient.expire(redisKey, this.redisConfig.ttl);
605
+ }
606
+ }
528
607
  }
529
608
  }
530
609
  }
@@ -545,62 +624,93 @@ export class RedisConversationMemoryManager {
545
624
  * Applies sendToolPreview toggle and hydrates result.result for backward compat
546
625
  */
547
626
  async buildContextMessages(sessionId, userId, enableSummarization, requestId) {
548
- logger.info("[RedisConversationMemoryManager] Building context messages", {
549
- sessionId,
550
- userId,
551
- method: "buildContextMessages",
552
- });
553
- const redisKey = getSessionKey(this.redisConfig, sessionId, userId);
554
- const conversationData = await this.redisClient?.get(redisKey);
555
- const conversation = deserializeConversation(conversationData || null);
556
- if (!conversation) {
627
+ await this.ensureInitialized();
628
+ if (!this.redisClient) {
629
+ logger.warn("[RedisConversationMemoryManager] Redis client not available in buildContextMessages");
557
630
  return [];
558
631
  }
559
- const session = {
560
- sessionId: conversation.sessionId,
561
- userId: conversation.userId,
562
- messages: conversation.messages,
563
- summarizedUpToMessageId: conversation.summarizedUpToMessageId,
564
- summarizedMessage: conversation.summarizedMessage,
565
- tokenThreshold: conversation.tokenThreshold,
566
- lastTokenCount: conversation.lastTokenCount,
567
- lastCountedAt: conversation.lastCountedAt,
568
- createdAt: new Date(conversation.createdAt).getTime(),
569
- lastActivity: new Date(conversation.updatedAt).getTime(),
570
- };
571
- const contextMessages = buildContextFromPointer(session, requestId);
572
- const sendToolPreview = this.config?.contextCompaction?.sendToolPreview === true;
573
- // Map tool_result messages: apply preview toggle + hydrate result.result
574
- const finalMessages = contextMessages.map((msg) => {
575
- if (msg.role !== "tool_result") {
576
- return msg;
577
- }
578
- // Toggle: swap content to preview if enabled AND a preview exists
579
- const content = sendToolPreview && msg.metadata?.toolOutputPreview
580
- ? msg.metadata.toolOutputPreview
581
- : msg.content;
582
- // Hydrate result.result from content for backward compatibility
583
- // (result.result is no longer stored — inferred from content at read time)
584
- let hydratedResult = msg.result;
585
- if (msg.result && msg.result.result === undefined) {
586
- let parsedResult = content;
587
- try {
588
- parsedResult = JSON.parse(content);
589
- }
590
- catch {
591
- /* plain text — use as-is */
632
+ // NLK-GAP-012: Add span for buildContext CRUD operation
633
+ return redisTracer.startActiveSpan("neurolink.memory.buildContext", {
634
+ kind: SpanKind.CLIENT,
635
+ attributes: {
636
+ "session.id": sessionId,
637
+ ...(userId && { "user.id": userId }),
638
+ },
639
+ }, async (span) => {
640
+ try {
641
+ logger.info("[RedisConversationMemoryManager] Building context messages", {
642
+ sessionId,
643
+ userId,
644
+ method: "buildContextMessages",
645
+ });
646
+ const redisKey = getSessionKey(this.redisConfig, sessionId, userId);
647
+ const conversationData = await this.redisClient.get(redisKey);
648
+ const conversation = deserializeConversation(conversationData || null);
649
+ if (!conversation) {
650
+ span.setAttribute("session.found", false);
651
+ span.setStatus({ code: SpanStatusCode.OK });
652
+ return [];
592
653
  }
593
- hydratedResult = { ...msg.result, result: parsedResult };
654
+ const session = {
655
+ sessionId: conversation.sessionId,
656
+ userId: conversation.userId,
657
+ messages: conversation.messages,
658
+ summarizedUpToMessageId: conversation.summarizedUpToMessageId,
659
+ summarizedMessage: conversation.summarizedMessage,
660
+ tokenThreshold: conversation.tokenThreshold,
661
+ lastTokenCount: conversation.lastTokenCount,
662
+ lastCountedAt: conversation.lastCountedAt,
663
+ createdAt: new Date(conversation.createdAt).getTime(),
664
+ lastActivity: new Date(conversation.updatedAt).getTime(),
665
+ };
666
+ const contextMessages = buildContextFromPointer(session, requestId);
667
+ const sendToolPreview = this.config?.contextCompaction?.sendToolPreview === true;
668
+ // Map tool_result messages: apply preview toggle + hydrate result.result
669
+ const finalMessages = contextMessages.map((msg) => {
670
+ if (msg.role !== "tool_result") {
671
+ return msg;
672
+ }
673
+ // Toggle: swap content to preview if enabled AND a preview exists
674
+ const content = sendToolPreview && msg.metadata?.toolOutputPreview
675
+ ? msg.metadata.toolOutputPreview
676
+ : msg.content;
677
+ // Hydrate result.result from content for backward compatibility
678
+ // (result.result is no longer stored — inferred from content at read time)
679
+ let hydratedResult = msg.result;
680
+ if (msg.result && msg.result.result === undefined) {
681
+ let parsedResult = content;
682
+ try {
683
+ parsedResult = JSON.parse(content);
684
+ }
685
+ catch {
686
+ /* plain text — use as-is */
687
+ }
688
+ hydratedResult = { ...msg.result, result: parsedResult };
689
+ }
690
+ return { ...msg, content, result: hydratedResult };
691
+ });
692
+ // Tool messages now have real content and participate in context properly.
693
+ // The tool output pruner (Stage 1) handles bounding old tool outputs.
694
+ span.setAttribute("context.message_count", finalMessages.length);
695
+ span.setStatus({ code: SpanStatusCode.OK });
696
+ logger.info("[RedisConversationMemoryManager] Retrieved context messages", {
697
+ sessionId,
698
+ userId,
699
+ });
700
+ return finalMessages;
701
+ }
702
+ catch (error) {
703
+ span.setStatus({
704
+ code: SpanStatusCode.ERROR,
705
+ message: error instanceof Error ? error.message : String(error),
706
+ });
707
+ span.recordException(error instanceof Error ? error : new Error(String(error)));
708
+ throw error;
709
+ }
710
+ finally {
711
+ span.end();
594
712
  }
595
- return { ...msg, content, result: hydratedResult };
596
- });
597
- // Tool messages now have real content and participate in context properly.
598
- // The tool output pruner (Stage 1) handles bounding old tool outputs.
599
- logger.info("[RedisConversationMemoryManager] Retrieved context messages", {
600
- sessionId,
601
- userId,
602
713
  });
603
- return finalMessages;
604
714
  }
605
715
  /**
606
716
  * Get session metadata for a specific user session (optimized for listing)
@@ -798,7 +908,7 @@ User message: "${userMessage}"`;
798
908
  input: { text: titlePrompt },
799
909
  provider: this.config.summarizationProvider || "vertex",
800
910
  model: this.config.summarizationModel || "gemini-2.5-flash",
801
- disableTools: false,
911
+ disableTools: true, // Title generation doesn't need tools — saves ~600 tokens of tool descriptions
802
912
  });
803
913
  // Clean up the generated title
804
914
  let title = result.content?.trim() || "New Conversation";
@@ -964,17 +1074,43 @@ User message: "${userMessage}"`;
964
1074
  if (!this.redisClient) {
965
1075
  return false;
966
1076
  }
967
- const redisKey = getSessionKey(this.redisConfig, sessionId, userId);
968
- const result = await this.redisClient.del(redisKey);
969
- if (result > 0) {
970
- // Remove session from user's session set
971
- if (userId) {
972
- await this.removeUserSession(userId, sessionId);
1077
+ // NLK-GAP-012: Add span for clearSession CRUD operation
1078
+ return redisTracer.startActiveSpan("neurolink.memory.clear", {
1079
+ kind: SpanKind.CLIENT,
1080
+ attributes: {
1081
+ "session.id": sessionId,
1082
+ ...(userId && { "user.id": userId }),
1083
+ },
1084
+ }, async (span) => {
1085
+ try {
1086
+ const redisKey = getSessionKey(this.redisConfig, sessionId, userId);
1087
+ const result = await this.redisClient.del(redisKey);
1088
+ if (result > 0) {
1089
+ // Remove session from user's session set
1090
+ if (userId) {
1091
+ await this.removeUserSession(userId, sessionId);
1092
+ }
1093
+ span.setAttribute("session.deleted", true);
1094
+ span.setStatus({ code: SpanStatusCode.OK });
1095
+ logger.info("Redis session cleared", { sessionId });
1096
+ return true;
1097
+ }
1098
+ span.setAttribute("session.deleted", false);
1099
+ span.setStatus({ code: SpanStatusCode.OK });
1100
+ return false;
973
1101
  }
974
- logger.info("Redis session cleared", { sessionId });
975
- return true;
976
- }
977
- return false;
1102
+ catch (error) {
1103
+ span.setStatus({
1104
+ code: SpanStatusCode.ERROR,
1105
+ message: error instanceof Error ? error.message : String(error),
1106
+ });
1107
+ span.recordException(error instanceof Error ? error : new Error(String(error)));
1108
+ throw error;
1109
+ }
1110
+ finally {
1111
+ span.end();
1112
+ }
1113
+ });
978
1114
  }
979
1115
  /**
980
1116
  * Clear all sessions