@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.
- package/CHANGELOG.md +12 -0
- package/README.md +15 -15
- package/dist/adapters/video/videoAnalyzer.d.ts +1 -1
- package/dist/adapters/video/videoAnalyzer.js +10 -8
- package/dist/auth/anthropicOAuth.d.ts +377 -0
- package/dist/auth/anthropicOAuth.js +914 -0
- package/dist/auth/index.d.ts +20 -0
- package/dist/auth/index.js +29 -0
- package/dist/auth/tokenStore.d.ts +225 -0
- package/dist/auth/tokenStore.js +521 -0
- package/dist/cli/commands/auth.d.ts +50 -0
- package/dist/cli/commands/auth.js +1115 -0
- package/dist/cli/commands/setup-anthropic.js +1 -14
- package/dist/cli/commands/setup-azure.js +1 -12
- package/dist/cli/commands/setup-bedrock.js +1 -9
- package/dist/cli/commands/setup-google-ai.js +1 -12
- package/dist/cli/commands/setup-openai.js +1 -14
- package/dist/cli/commands/workflow.d.ts +27 -0
- package/dist/cli/commands/workflow.js +216 -0
- package/dist/cli/factories/authCommandFactory.d.ts +52 -0
- package/dist/cli/factories/authCommandFactory.js +146 -0
- package/dist/cli/factories/commandFactory.d.ts +6 -0
- package/dist/cli/factories/commandFactory.js +171 -22
- package/dist/cli/index.js +0 -1
- package/dist/cli/parser.js +14 -2
- package/dist/cli/utils/maskCredential.d.ts +11 -0
- package/dist/cli/utils/maskCredential.js +23 -0
- package/dist/constants/contextWindows.js +107 -16
- package/dist/constants/enums.d.ts +119 -15
- package/dist/constants/enums.js +182 -22
- package/dist/constants/index.d.ts +3 -1
- package/dist/constants/index.js +11 -1
- package/dist/context/budgetChecker.js +1 -1
- package/dist/context/contextCompactor.js +31 -4
- package/dist/context/emergencyTruncation.d.ts +21 -0
- package/dist/context/emergencyTruncation.js +88 -0
- package/dist/context/errorDetection.d.ts +16 -0
- package/dist/context/errorDetection.js +48 -1
- package/dist/context/errors.d.ts +19 -0
- package/dist/context/errors.js +21 -0
- package/dist/context/stages/slidingWindowTruncator.d.ts +6 -0
- package/dist/context/stages/slidingWindowTruncator.js +159 -24
- package/dist/core/baseProvider.js +306 -200
- package/dist/core/conversationMemoryManager.js +104 -61
- package/dist/core/evaluationProviders.js +16 -33
- package/dist/core/factory.js +237 -164
- package/dist/core/modules/GenerationHandler.js +175 -116
- package/dist/core/modules/MessageBuilder.js +222 -170
- package/dist/core/modules/StreamHandler.d.ts +1 -0
- package/dist/core/modules/StreamHandler.js +95 -27
- package/dist/core/modules/TelemetryHandler.d.ts +10 -1
- package/dist/core/modules/TelemetryHandler.js +25 -7
- package/dist/core/modules/ToolsManager.js +115 -191
- package/dist/core/redisConversationMemoryManager.js +418 -282
- package/dist/factories/providerRegistry.d.ts +5 -0
- package/dist/factories/providerRegistry.js +20 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +4 -2
- package/dist/lib/adapters/video/videoAnalyzer.d.ts +1 -1
- package/dist/lib/adapters/video/videoAnalyzer.js +10 -8
- package/dist/lib/auth/anthropicOAuth.d.ts +377 -0
- package/dist/lib/auth/anthropicOAuth.js +915 -0
- package/dist/lib/auth/index.d.ts +20 -0
- package/dist/lib/auth/index.js +30 -0
- package/dist/lib/auth/tokenStore.d.ts +225 -0
- package/dist/lib/auth/tokenStore.js +522 -0
- package/dist/lib/constants/contextWindows.js +107 -16
- package/dist/lib/constants/enums.d.ts +119 -15
- package/dist/lib/constants/enums.js +182 -22
- package/dist/lib/constants/index.d.ts +3 -1
- package/dist/lib/constants/index.js +11 -1
- package/dist/lib/context/budgetChecker.js +1 -1
- package/dist/lib/context/contextCompactor.js +31 -4
- package/dist/lib/context/emergencyTruncation.d.ts +21 -0
- package/dist/lib/context/emergencyTruncation.js +89 -0
- package/dist/lib/context/errorDetection.d.ts +16 -0
- package/dist/lib/context/errorDetection.js +48 -1
- package/dist/lib/context/errors.d.ts +19 -0
- package/dist/lib/context/errors.js +22 -0
- package/dist/lib/context/stages/slidingWindowTruncator.d.ts +6 -0
- package/dist/lib/context/stages/slidingWindowTruncator.js +159 -24
- package/dist/lib/core/baseProvider.js +306 -200
- package/dist/lib/core/conversationMemoryManager.js +104 -61
- package/dist/lib/core/evaluationProviders.js +16 -33
- package/dist/lib/core/factory.js +237 -164
- package/dist/lib/core/modules/GenerationHandler.js +175 -116
- package/dist/lib/core/modules/MessageBuilder.js +222 -170
- package/dist/lib/core/modules/StreamHandler.d.ts +1 -0
- package/dist/lib/core/modules/StreamHandler.js +95 -27
- package/dist/lib/core/modules/TelemetryHandler.d.ts +10 -1
- package/dist/lib/core/modules/TelemetryHandler.js +25 -7
- package/dist/lib/core/modules/ToolsManager.js +115 -191
- package/dist/lib/core/redisConversationMemoryManager.js +418 -282
- package/dist/lib/factories/providerRegistry.d.ts +5 -0
- package/dist/lib/factories/providerRegistry.js +20 -2
- package/dist/lib/index.d.ts +3 -3
- package/dist/lib/index.js +4 -2
- package/dist/lib/mcp/externalServerManager.js +66 -0
- package/dist/lib/mcp/mcpCircuitBreaker.js +24 -0
- package/dist/lib/mcp/mcpClientFactory.js +16 -0
- package/dist/lib/mcp/toolDiscoveryService.js +32 -6
- package/dist/lib/mcp/toolRegistry.js +193 -123
- package/dist/lib/models/anthropicModels.d.ts +267 -0
- package/dist/lib/models/anthropicModels.js +528 -0
- package/dist/lib/neurolink.d.ts +6 -0
- package/dist/lib/neurolink.js +1162 -646
- package/dist/lib/providers/amazonBedrock.d.ts +1 -1
- package/dist/lib/providers/amazonBedrock.js +521 -319
- package/dist/lib/providers/anthropic.d.ts +123 -2
- package/dist/lib/providers/anthropic.js +873 -27
- package/dist/lib/providers/anthropicBaseProvider.js +77 -17
- package/dist/lib/providers/googleAiStudio.d.ts +1 -1
- package/dist/lib/providers/googleAiStudio.js +292 -227
- package/dist/lib/providers/googleVertex.d.ts +36 -1
- package/dist/lib/providers/googleVertex.js +553 -260
- package/dist/lib/providers/ollama.js +329 -278
- package/dist/lib/providers/openAI.js +77 -19
- package/dist/lib/providers/sagemaker/parsers.js +3 -3
- package/dist/lib/providers/sagemaker/streaming.js +3 -3
- package/dist/lib/proxy/proxyFetch.js +81 -48
- package/dist/lib/rag/ChunkerFactory.js +1 -1
- package/dist/lib/rag/chunkers/MarkdownChunker.d.ts +22 -0
- package/dist/lib/rag/chunkers/MarkdownChunker.js +213 -9
- package/dist/lib/rag/chunking/markdownChunker.d.ts +16 -0
- package/dist/lib/rag/chunking/markdownChunker.js +174 -2
- package/dist/lib/rag/pipeline/contextAssembly.js +2 -1
- package/dist/lib/rag/ragIntegration.d.ts +18 -1
- package/dist/lib/rag/ragIntegration.js +94 -14
- package/dist/lib/rag/retrieval/vectorQueryTool.js +21 -4
- package/dist/lib/server/abstract/baseServerAdapter.js +4 -1
- package/dist/lib/server/adapters/fastifyAdapter.js +35 -30
- package/dist/lib/services/server/ai/observability/instrumentation.d.ts +32 -0
- package/dist/lib/services/server/ai/observability/instrumentation.js +39 -0
- package/dist/lib/telemetry/attributes.d.ts +52 -0
- package/dist/lib/telemetry/attributes.js +61 -0
- package/dist/lib/telemetry/index.d.ts +3 -0
- package/dist/lib/telemetry/index.js +3 -0
- package/dist/lib/telemetry/telemetryService.d.ts +6 -0
- package/dist/lib/telemetry/telemetryService.js +6 -0
- package/dist/lib/telemetry/tracers.d.ts +15 -0
- package/dist/lib/telemetry/tracers.js +17 -0
- package/dist/lib/telemetry/withSpan.d.ts +9 -0
- package/dist/lib/telemetry/withSpan.js +35 -0
- package/dist/lib/types/contextTypes.d.ts +10 -0
- package/dist/lib/types/errors.d.ts +62 -0
- package/dist/lib/types/errors.js +107 -0
- package/dist/lib/types/index.d.ts +2 -1
- package/dist/lib/types/index.js +2 -0
- package/dist/lib/types/providers.d.ts +107 -0
- package/dist/lib/types/providers.js +69 -0
- package/dist/lib/types/streamTypes.d.ts +14 -0
- package/dist/lib/types/subscriptionTypes.d.ts +893 -0
- package/dist/lib/types/subscriptionTypes.js +8 -0
- package/dist/lib/utils/conversationMemory.js +121 -82
- package/dist/lib/utils/logger.d.ts +5 -0
- package/dist/lib/utils/logger.js +50 -2
- package/dist/lib/utils/messageBuilder.js +22 -42
- package/dist/lib/utils/modelDetection.js +3 -3
- package/dist/lib/utils/providerConfig.d.ts +167 -0
- package/dist/lib/utils/providerConfig.js +619 -9
- package/dist/lib/utils/providerRetry.d.ts +41 -0
- package/dist/lib/utils/providerRetry.js +114 -0
- package/dist/lib/utils/retryability.d.ts +14 -0
- package/dist/lib/utils/retryability.js +23 -0
- package/dist/lib/utils/sanitizers/svg.js +4 -5
- package/dist/lib/utils/tokenEstimation.d.ts +11 -1
- package/dist/lib/utils/tokenEstimation.js +19 -4
- package/dist/lib/utils/videoAnalysisProcessor.js +7 -3
- package/dist/mcp/externalServerManager.js +66 -0
- package/dist/mcp/mcpCircuitBreaker.js +24 -0
- package/dist/mcp/mcpClientFactory.js +16 -0
- package/dist/mcp/toolDiscoveryService.js +32 -6
- package/dist/mcp/toolRegistry.js +193 -123
- package/dist/models/anthropicModels.d.ts +267 -0
- package/dist/models/anthropicModels.js +527 -0
- package/dist/neurolink.d.ts +6 -0
- package/dist/neurolink.js +1162 -646
- package/dist/providers/amazonBedrock.d.ts +1 -1
- package/dist/providers/amazonBedrock.js +521 -319
- package/dist/providers/anthropic.d.ts +123 -2
- package/dist/providers/anthropic.js +873 -27
- package/dist/providers/anthropicBaseProvider.js +77 -17
- package/dist/providers/googleAiStudio.d.ts +1 -1
- package/dist/providers/googleAiStudio.js +292 -227
- package/dist/providers/googleVertex.d.ts +36 -1
- package/dist/providers/googleVertex.js +553 -260
- package/dist/providers/ollama.js +329 -278
- package/dist/providers/openAI.js +77 -19
- package/dist/providers/sagemaker/parsers.js +3 -3
- package/dist/providers/sagemaker/streaming.js +3 -3
- package/dist/proxy/proxyFetch.js +81 -48
- package/dist/rag/ChunkerFactory.js +1 -1
- package/dist/rag/chunkers/MarkdownChunker.d.ts +22 -0
- package/dist/rag/chunkers/MarkdownChunker.js +213 -9
- package/dist/rag/chunking/markdownChunker.d.ts +16 -0
- package/dist/rag/chunking/markdownChunker.js +174 -2
- package/dist/rag/pipeline/contextAssembly.js +2 -1
- package/dist/rag/ragIntegration.d.ts +18 -1
- package/dist/rag/ragIntegration.js +94 -14
- package/dist/rag/retrieval/vectorQueryTool.js +21 -4
- package/dist/server/abstract/baseServerAdapter.js +4 -1
- package/dist/server/adapters/fastifyAdapter.js +35 -30
- package/dist/services/server/ai/observability/instrumentation.d.ts +32 -0
- package/dist/services/server/ai/observability/instrumentation.js +39 -0
- package/dist/telemetry/attributes.d.ts +52 -0
- package/dist/telemetry/attributes.js +60 -0
- package/dist/telemetry/index.d.ts +3 -0
- package/dist/telemetry/index.js +3 -0
- package/dist/telemetry/telemetryService.d.ts +6 -0
- package/dist/telemetry/telemetryService.js +6 -0
- package/dist/telemetry/tracers.d.ts +15 -0
- package/dist/telemetry/tracers.js +16 -0
- package/dist/telemetry/withSpan.d.ts +9 -0
- package/dist/telemetry/withSpan.js +34 -0
- package/dist/types/contextTypes.d.ts +10 -0
- package/dist/types/errors.d.ts +62 -0
- package/dist/types/errors.js +107 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/index.js +2 -0
- package/dist/types/providers.d.ts +107 -0
- package/dist/types/providers.js +69 -0
- package/dist/types/streamTypes.d.ts +14 -0
- package/dist/types/subscriptionTypes.d.ts +893 -0
- package/dist/types/subscriptionTypes.js +7 -0
- package/dist/utils/conversationMemory.js +121 -82
- package/dist/utils/logger.d.ts +5 -0
- package/dist/utils/logger.js +50 -2
- package/dist/utils/messageBuilder.js +22 -42
- package/dist/utils/modelDetection.js +3 -3
- package/dist/utils/providerConfig.d.ts +167 -0
- package/dist/utils/providerConfig.js +619 -9
- package/dist/utils/providerRetry.d.ts +41 -0
- package/dist/utils/providerRetry.js +113 -0
- package/dist/utils/retryability.d.ts +14 -0
- package/dist/utils/retryability.js +22 -0
- package/dist/utils/sanitizers/svg.js +4 -5
- package/dist/utils/tokenEstimation.d.ts +11 -1
- package/dist/utils/tokenEstimation.js +19 -4
- package/dist/utils/videoAnalysisProcessor.js +7 -3
- package/dist/workflow/config.d.ts +26 -26
- 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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
recentMessageCount
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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.
|
|
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 (
|
|
436
|
-
logger.
|
|
437
|
-
sessionId:
|
|
438
|
-
|
|
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
|
-
|
|
444
|
-
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
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
|
-
|
|
471
|
-
|
|
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
|
-
|
|
474
|
-
|
|
549
|
+
finally {
|
|
550
|
+
span.end();
|
|
475
551
|
}
|
|
476
|
-
|
|
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
|
-
|
|
525
|
-
await this.redisClient.
|
|
526
|
-
if (
|
|
527
|
-
|
|
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
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
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
|