@juspay/neurolink 12.9.6 → 12.11.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 (39) hide show
  1. package/CHANGELOG.md +3 -3
  2. package/dist/artifacts/artifactBanking.d.ts +6 -2
  3. package/dist/artifacts/artifactBanking.js +9 -14
  4. package/dist/artifacts/artifactReader.d.ts +69 -0
  5. package/dist/artifacts/artifactReader.js +157 -0
  6. package/dist/artifacts/artifactStore.d.ts +7 -3
  7. package/dist/artifacts/artifactStore.js +10 -21
  8. package/dist/artifacts/artifactStoreFactory.d.ts +39 -0
  9. package/dist/artifacts/artifactStoreFactory.js +101 -0
  10. package/dist/artifacts/redisArtifactStore.d.ts +84 -0
  11. package/dist/artifacts/redisArtifactStore.js +270 -0
  12. package/dist/browser/neurolink.min.js +381 -383
  13. package/dist/constants/enums.d.ts +77 -0
  14. package/dist/constants/enums.js +82 -0
  15. package/dist/core/modules/GenerationHandler.d.ts +15 -3
  16. package/dist/core/modules/GenerationHandler.js +172 -14
  17. package/dist/index.d.ts +3 -0
  18. package/dist/index.js +7 -0
  19. package/dist/memory/memoryRetrievalTools.js +104 -35
  20. package/dist/neurolink.d.ts +45 -1
  21. package/dist/neurolink.js +128 -18
  22. package/dist/providers/catalog/baseten.json +263 -0
  23. package/dist/providers/catalog/gmicloud.json +64 -0
  24. package/dist/providers/catalog/inception-labs.json +75 -0
  25. package/dist/providers/catalog/index.generated.d.ts +1 -1
  26. package/dist/providers/catalog/index.generated.js +15 -0
  27. package/dist/providers/catalog/io-intelligence.json +463 -0
  28. package/dist/providers/catalog/schema.d.ts +1 -1
  29. package/dist/providers/catalog/upstage.json +165 -0
  30. package/dist/providers/openaiChatCompletionsClient.js +18 -1
  31. package/dist/types/artifact.d.ts +124 -3
  32. package/dist/types/config.d.ts +7 -0
  33. package/dist/types/generate.d.ts +17 -0
  34. package/dist/types/openaiCompatible.d.ts +5 -1
  35. package/dist/types/providerCatalog.generated.d.ts +2 -2
  36. package/dist/types/providers.d.ts +20 -0
  37. package/dist/utils/redis.d.ts +15 -0
  38. package/dist/utils/redis.js +64 -6
  39. package/package.json +10 -6
@@ -6,12 +6,15 @@ import { SpanSerializer, SpanType, SpanStatus, getMetricsAggregator, } from "../
6
6
  import { withSpan } from "../telemetry/withSpan.js";
7
7
  import { tracers } from "../telemetry/tracers.js";
8
8
  import { tool } from "../utils/tool.js";
9
+ import { MAX_ARTIFACT_SEARCH_MATCHES, readArtifactWindow, searchArtifactContent, validateSearchPattern, } from "../artifacts/artifactReader.js";
9
10
  /** Maximum characters returned per retrieval request */
10
11
  const DEFAULT_RETRIEVAL_LIMIT = 50_000;
11
12
  /** Hard maximum for user/LLM-supplied limit to prevent massive tool outputs */
12
13
  const MAX_RETRIEVAL_LIMIT = 200_000;
13
14
  /** Maximum number of search matches returned */
14
15
  const MAX_SEARCH_MATCHES = 50;
16
+ /** Bound on one artifact backend round trip, so a stalled store never hangs the tool. */
17
+ const ARTIFACT_READ_TIMEOUT_MS = 10_000;
15
18
  /**
16
19
  * Factory function that creates memory retrieval tools bound to a memory manager.
17
20
  *
@@ -27,13 +30,16 @@ const MAX_SEARCH_MATCHES = 50;
27
30
  export function createMemoryRetrievalTools(memoryManager, artifactStore) {
28
31
  return {
29
32
  retrieve_context: tool({
30
- description: "Retrieve messages from conversation memory, or fetch the full payload of " +
31
- "an externalized MCP tool output by artifact ID. Use this to:\n" +
33
+ description: "Retrieve messages from conversation memory, or read an externalized " +
34
+ "tool output / banked payload by artifact ID. Use this to:\n" +
32
35
  "• Access full tool outputs when a result was truncated or externalized\n" +
33
36
  "• Review previous assistant responses\n" +
34
- "• Search through conversation history\n" +
35
- "Supports filtering by role, pagination for large content, and regex search.\n" +
36
- "To fetch an externalized artifact, provide `artifactId` (omit sessionId).",
37
+ "• Search a session's history, or an artifact, for literal text\n" +
38
+ "Supports filtering by role, offset/limit pagination for large content, " +
39
+ "and case-insensitive literal search (not regex).\n" +
40
+ "To read an artifact, provide `artifactId` (omit sessionId): pass " +
41
+ "`offset`/`limit` to page, or `search` to get match offsets and jump " +
42
+ "straight to them instead of paging.",
37
43
  inputSchema: z.object({
38
44
  sessionId: z
39
45
  .string()
@@ -44,8 +50,9 @@ export function createMemoryRetrievalTools(memoryManager, artifactStore) {
44
50
  .string()
45
51
  .optional()
46
52
  .describe("Artifact ID from an externalized MCP tool output " +
47
- "(visible in the tool output as neurolinkArtifactId=<id>). " +
48
- "When provided, returns the full stored payload directly."),
53
+ "(visible in the tool output as neurolinkArtifactId=<id>) or a " +
54
+ "banked payload. When provided, reads the stored payload: a " +
55
+ "window at `offset`/`limit`, or with `search`, the matches."),
49
56
  messageId: z
50
57
  .string()
51
58
  .optional()
@@ -75,8 +82,13 @@ export function createMemoryRetrievalTools(memoryManager, artifactStore) {
75
82
  search: z
76
83
  .string()
77
84
  .optional()
78
- .describe("Regex pattern to search within message content. " +
79
- "Returns matching lines with line numbers."),
85
+ .describe("Case-insensitive literal text to find (regex metacharacters are " +
86
+ "matched literally). Session history: returns matching lines " +
87
+ "with line numbers. Artifact: returns up to " +
88
+ `${MAX_ARTIFACT_SEARCH_MATCHES} matches, each with the character ` +
89
+ "`offset` of the hit and a short snippet — pass that offset back " +
90
+ "as `offset` to read around it. With `offset`, the artifact " +
91
+ "search starts there; use `nextSearchOffset` to continue."),
80
92
  }),
81
93
  execute: async (args) => withSpan({
82
94
  name: "neurolink.memory.retrieve_context",
@@ -108,35 +120,12 @@ async function executeRetrieveContext(args, memoryManager, artifactStore, otelSp
108
120
  message: "Artifact store not configured",
109
121
  });
110
122
  return {
111
- error: "Artifact store not configured — " +
112
- "mcp.outputLimits.strategy must be set to 'externalize' to use artifactId retrieval",
123
+ error: "Artifact store not configured — this instance has never banked " +
124
+ "or externalized anything, so there is no artifact to read",
113
125
  artifactId: args.artifactId,
114
126
  };
115
127
  }
116
- const content = await withTimeout(artifactStore.retrieve(args.artifactId), 10_000, new Error(`ArtifactStore.retrieve() timed out for artifact "${args.artifactId}"`));
117
- if (content === null) {
118
- otelSpan.setStatus({
119
- code: SpanStatusCode.ERROR,
120
- message: "Artifact not found or has expired",
121
- });
122
- return {
123
- error: "Artifact not found or has expired",
124
- artifactId: args.artifactId,
125
- };
126
- }
127
- const charLimit = Math.min(args.limit ?? DEFAULT_RETRIEVAL_LIMIT, MAX_RETRIEVAL_LIMIT);
128
- const start = args.offset ?? 0;
129
- const slice = content.slice(start, start + charLimit);
130
- otelSpan.setAttribute("memory.artifact_size", content.length);
131
- otelSpan.setAttribute("memory.returned_bytes", slice.length);
132
- return {
133
- artifactId: args.artifactId,
134
- content: slice,
135
- totalSize: content.length,
136
- hasMore: start + charLimit < content.length,
137
- offset: start,
138
- limit: charLimit,
139
- };
128
+ return executeArtifactRetrieval(args.artifactId, args, artifactStore, otelSpan);
140
129
  }
141
130
  // ── End artifact resolution ─────────────────────────────────────────
142
131
  if (!args.sessionId) {
@@ -272,3 +261,83 @@ async function executeRetrieveContext(args, memoryManager, artifactStore, otelSp
272
261
  return { error: "Failed to retrieve context" };
273
262
  }
274
263
  }
264
+ /**
265
+ * The artifact branch of `retrieve_context`.
266
+ *
267
+ * Two modes, chosen by `search`:
268
+ * - Paged read: one window through `readArtifactWindow`, which lets a backend
269
+ * with range reads move only the window. `hasMore` comes from the window's
270
+ * `totalLength`, so it never needs the payload either.
271
+ * - Search: the whole payload is read once and scanned for the literal
272
+ * pattern; the model gets match offsets and bounded snippets and can jump
273
+ * straight to the hit with `offset` instead of paging to it. Before this
274
+ * branch existed `search` was accepted and silently ignored here, and a
275
+ * model could not tell — an unfiltered window looks like "no matches".
276
+ *
277
+ * Backend failures (a Redis outage, a timeout) are reported as errors, never
278
+ * as "not found": those are different facts and the model acts differently on
279
+ * each.
280
+ */
281
+ async function executeArtifactRetrieval(artifactId, args, artifactStore, otelSpan) {
282
+ const notFound = () => {
283
+ otelSpan.setStatus({
284
+ code: SpanStatusCode.ERROR,
285
+ message: "Artifact not found or has expired",
286
+ });
287
+ return { error: "Artifact not found or has expired", artifactId };
288
+ };
289
+ try {
290
+ if (args.search !== undefined) {
291
+ const invalid = validateSearchPattern(args.search);
292
+ if (invalid) {
293
+ otelSpan.setStatus({ code: SpanStatusCode.ERROR, message: invalid });
294
+ return { error: invalid, artifactId };
295
+ }
296
+ // A search has to see the whole payload; the window contract is for reads.
297
+ const content = await withTimeout(artifactStore.retrieve(artifactId), ARTIFACT_READ_TIMEOUT_MS, new Error(`ArtifactStore.retrieve() timed out for artifact "${artifactId}"`));
298
+ if (content === null) {
299
+ return notFound();
300
+ }
301
+ const result = searchArtifactContent(content, args.search, {
302
+ from: args.offset,
303
+ });
304
+ otelSpan.setAttribute("memory.artifact_size", content.length);
305
+ otelSpan.setAttribute("memory.search_matches", result.totalMatches);
306
+ return {
307
+ artifactId,
308
+ search: args.search,
309
+ totalSize: content.length,
310
+ ...result,
311
+ };
312
+ }
313
+ const charLimit = Math.min(args.limit ?? DEFAULT_RETRIEVAL_LIMIT, MAX_RETRIEVAL_LIMIT);
314
+ const start = Math.max(0, args.offset ?? 0);
315
+ const window = await withTimeout(readArtifactWindow(artifactStore, artifactId, {
316
+ offset: start,
317
+ limit: charLimit,
318
+ }), ARTIFACT_READ_TIMEOUT_MS, new Error(`Artifact read timed out for artifact "${artifactId}"`));
319
+ if (window === null) {
320
+ return notFound();
321
+ }
322
+ otelSpan.setAttribute("memory.artifact_size", window.totalLength);
323
+ otelSpan.setAttribute("memory.returned_bytes", window.content.length);
324
+ otelSpan.setAttribute("memory.artifact_range_read", typeof artifactStore.retrieveRange === "function");
325
+ return {
326
+ artifactId,
327
+ content: window.content,
328
+ totalSize: window.totalLength,
329
+ hasMore: window.offset + window.content.length < window.totalLength,
330
+ offset: window.offset,
331
+ limit: charLimit,
332
+ };
333
+ }
334
+ catch (error) {
335
+ const message = error instanceof Error ? error.message : String(error);
336
+ logger.error("[MemoryRetrievalTools] Artifact read failed", {
337
+ artifactId,
338
+ error: message,
339
+ });
340
+ otelSpan.setStatus({ code: SpanStatusCode.ERROR, message });
341
+ return { error: `Artifact read failed: ${message}`, artifactId };
342
+ }
343
+ }
@@ -82,8 +82,19 @@ export declare class NeuroLink {
82
82
  private mcpToolBatcher?;
83
83
  private mcpEnhancedDiscovery?;
84
84
  private mcpToolMiddlewares;
85
- /** Artifact store for externalized MCP tool outputs (set when strategy=externalize). */
85
+ /** Artifact store for externalized MCP tool outputs and banked payloads. */
86
86
  private mcpArtifactStore?;
87
+ /** `artifacts` constructor config — consulted whenever a store is created. */
88
+ private artifactsConfig?;
89
+ /**
90
+ * True when this instance built `mcpArtifactStore` itself (local or Redis
91
+ * from config / STORAGE_TYPE). NeuroLink closes only stores it built — on
92
+ * replacement and on shutdown; a store handed in through `artifacts.store`
93
+ * or `setArtifactStore()` is the caller's to close.
94
+ */
95
+ private ownsArtifactStore;
96
+ /** Normalizer settings, kept so `setArtifactStore()` can rebuild it. */
97
+ private mcpOutputNormalizerConfig?;
87
98
  private _disableToolCacheForCurrentRequest;
88
99
  /**
89
100
  * (toolName + args) keys already served during the CURRENT request.
@@ -2735,6 +2746,39 @@ export declare class NeuroLink {
2735
2746
  * @returns The artifact store backing {@link bankArtifact} / {@link readArtifact}
2736
2747
  */
2737
2748
  getArtifactStore(): ArtifactStore;
2749
+ /**
2750
+ * Replace this instance's artifact store.
2751
+ *
2752
+ * Everything that writes or reads artifacts follows the swap: banking,
2753
+ * `retrieve_context`, host-side `readArtifact`, and the MCP output
2754
+ * normalizer — which is rebuilt here because it captured the previous store
2755
+ * at construction. Assigning the field alone would miss it, and
2756
+ * externalized tool outputs would keep landing in the old backend while
2757
+ * read-backs looked in the new one.
2758
+ *
2759
+ * Call it before the first bank or externalized tool output: artifacts
2760
+ * already in the previous store are not migrated, and their ids stop
2761
+ * resolving through this instance. `artifacts.store` in the constructor
2762
+ * config is the same thing without the ordering concern.
2763
+ *
2764
+ * Ownership: a store you hand in — here or via `artifacts.store` — stays
2765
+ * yours to close. NeuroLink closes only the stores it built itself, when
2766
+ * they are replaced here and on `shutdown()`.
2767
+ *
2768
+ * @param store - Any {@link ArtifactStore}
2769
+ */
2770
+ setArtifactStore(store: ArtifactStore): void;
2771
+ /**
2772
+ * Build the store the `artifacts` config (or `STORAGE_TYPE`) asks for. The
2773
+ * Redis connection falls back to conversation memory's, so one
2774
+ * `STORAGE_TYPE=redis` moves sessions and artifacts together.
2775
+ */
2776
+ private createConfiguredArtifactStore;
2777
+ /**
2778
+ * (Re)build the MCP output normalizer over `artifactStore`. A no-op unless
2779
+ * `mcp.outputLimits` was configured — without it nothing is externalized.
2780
+ */
2781
+ private installOutputNormalizer;
2738
2782
  /**
2739
2783
  * Bank a payload to a file and get back a pointer to it.
2740
2784
  *
package/dist/neurolink.js CHANGED
@@ -47,7 +47,7 @@ import { ToolResultCache } from "./mcp/caching/index.js";
47
47
  import { EnhancedToolDiscovery } from "./mcp/enhancedToolDiscovery.js";
48
48
  import { ExternalServerManager } from "./mcp/externalServerManager.js";
49
49
  import { McpOutputNormalizer, DEFAULT_MAX_MCP_OUTPUT_BYTES, DEFAULT_WARN_MCP_OUTPUT_BYTES, } from "./mcp/mcpOutputNormalizer.js";
50
- import { LocalTempArtifactStore } from "./artifacts/artifactStore.js";
50
+ import { createArtifactStore, resolveArtifactStorageType, } from "./artifacts/artifactStoreFactory.js";
51
51
  import { bankArtifact as bankArtifactPayload, readArtifact as readBankedArtifact, } from "./artifacts/artifactBanking.js";
52
52
  import { ToolRouter } from "./mcp/routing/index.js";
53
53
  // Import direct tools server for automatic registration
@@ -380,8 +380,19 @@ export class NeuroLink {
380
380
  mcpToolBatcher;
381
381
  mcpEnhancedDiscovery;
382
382
  mcpToolMiddlewares = [];
383
- /** Artifact store for externalized MCP tool outputs (set when strategy=externalize). */
383
+ /** Artifact store for externalized MCP tool outputs and banked payloads. */
384
384
  mcpArtifactStore;
385
+ /** `artifacts` constructor config — consulted whenever a store is created. */
386
+ artifactsConfig;
387
+ /**
388
+ * True when this instance built `mcpArtifactStore` itself (local or Redis
389
+ * from config / STORAGE_TYPE). NeuroLink closes only stores it built — on
390
+ * replacement and on shutdown; a store handed in through `artifacts.store`
391
+ * or `setArtifactStore()` is the caller's to close.
392
+ */
393
+ ownsArtifactStore = false;
394
+ /** Normalizer settings, kept so `setArtifactStore()` can rebuild it. */
395
+ mcpOutputNormalizerConfig;
385
396
  _disableToolCacheForCurrentRequest = false;
386
397
  /**
387
398
  * (toolName + args) keys already served during the CURRENT request.
@@ -1198,24 +1209,22 @@ export class NeuroLink {
1198
1209
  });
1199
1210
  }
1200
1211
  // ToolRouter — lazy-initialized when 2+ external servers exist (see addExternalMCPServer)
1212
+ // Artifact storage — where externalized MCP outputs and banked payloads
1213
+ // live. Kept so both creation sites (externalize here, and
1214
+ // getArtifactStore() on demand) resolve the same backend.
1215
+ this.artifactsConfig = config?.artifacts;
1201
1216
  // McpOutputNormalizer — active when mcp.outputLimits is configured
1202
1217
  if (mcpConfig?.outputLimits) {
1203
1218
  const strategy = mcpConfig.outputLimits.strategy ?? "externalize";
1204
1219
  const maxBytes = mcpConfig.outputLimits.maxBytes ?? DEFAULT_MAX_MCP_OUTPUT_BYTES;
1205
1220
  const warnBytes = mcpConfig.outputLimits.warnBytes ?? DEFAULT_WARN_MCP_OUTPUT_BYTES;
1221
+ this.mcpOutputNormalizerConfig = { strategy, maxBytes, warnBytes };
1206
1222
  let artifactStore;
1207
1223
  if (strategy === "externalize") {
1208
- artifactStore = new LocalTempArtifactStore();
1224
+ artifactStore = this.createConfiguredArtifactStore();
1209
1225
  this.mcpArtifactStore = artifactStore;
1210
- logger.debug("[NeuroLink] MCP artifact store initialized (local-temp)");
1211
1226
  }
1212
- const normalizer = new McpOutputNormalizer({ strategy, maxBytes, warnBytes }, artifactStore);
1213
- this.externalServerManager.setOutputNormalizer(normalizer);
1214
- logger.debug("[NeuroLink] MCP output normalizer initialized", {
1215
- strategy,
1216
- maxBytes,
1217
- warnBytes,
1218
- });
1227
+ this.installOutputNormalizer(artifactStore);
1219
1228
  }
1220
1229
  }
1221
1230
  /**
@@ -2806,6 +2815,17 @@ Current user's request: ${currentInput}`;
2806
2815
  this._taskManager = undefined;
2807
2816
  }
2808
2817
  }
2818
+ // Release the artifact store this instance built (a pooled Redis
2819
+ // connection, for one). An injected store is the caller's to close.
2820
+ if (this.ownsArtifactStore && this.mcpArtifactStore?.close) {
2821
+ try {
2822
+ await this.mcpArtifactStore.close();
2823
+ logger.debug("[NeuroLink] Artifact store closed");
2824
+ }
2825
+ catch (error) {
2826
+ logger.warn("[NeuroLink] Artifact store close failed:", error);
2827
+ }
2828
+ }
2809
2829
  // Close conversation memory manager (release Redis connections, etc.)
2810
2830
  if (this.conversationMemory?.close) {
2811
2831
  try {
@@ -13010,13 +13030,86 @@ Current user's request: ${currentInput}`;
13010
13030
  */
13011
13031
  getArtifactStore() {
13012
13032
  if (!this.mcpArtifactStore) {
13013
- this.mcpArtifactStore = new LocalTempArtifactStore();
13014
- logger.debug("[NeuroLink] Artifact store created on demand (local-temp) for banking");
13033
+ this.mcpArtifactStore = this.createConfiguredArtifactStore();
13015
13034
  }
13016
13035
  // A no-op when the constructor already registered it.
13017
13036
  this.registerMemoryRetrievalTools();
13018
13037
  return this.mcpArtifactStore;
13019
13038
  }
13039
+ /**
13040
+ * Replace this instance's artifact store.
13041
+ *
13042
+ * Everything that writes or reads artifacts follows the swap: banking,
13043
+ * `retrieve_context`, host-side `readArtifact`, and the MCP output
13044
+ * normalizer — which is rebuilt here because it captured the previous store
13045
+ * at construction. Assigning the field alone would miss it, and
13046
+ * externalized tool outputs would keep landing in the old backend while
13047
+ * read-backs looked in the new one.
13048
+ *
13049
+ * Call it before the first bank or externalized tool output: artifacts
13050
+ * already in the previous store are not migrated, and their ids stop
13051
+ * resolving through this instance. `artifacts.store` in the constructor
13052
+ * config is the same thing without the ordering concern.
13053
+ *
13054
+ * Ownership: a store you hand in — here or via `artifacts.store` — stays
13055
+ * yours to close. NeuroLink closes only the stores it built itself, when
13056
+ * they are replaced here and on `shutdown()`.
13057
+ *
13058
+ * @param store - Any {@link ArtifactStore}
13059
+ */
13060
+ setArtifactStore(store) {
13061
+ const previous = this.mcpArtifactStore;
13062
+ if (previous === store) {
13063
+ return;
13064
+ }
13065
+ if (previous) {
13066
+ logger.warn("[NeuroLink] Artifact store replaced — artifacts already written to " +
13067
+ "the previous store will not resolve through this instance");
13068
+ if (this.ownsArtifactStore && previous.close) {
13069
+ void previous.close().catch((error) => {
13070
+ logger.warn("[NeuroLink] Previous artifact store close failed", {
13071
+ error: error instanceof Error ? error.message : String(error),
13072
+ });
13073
+ });
13074
+ }
13075
+ }
13076
+ this.mcpArtifactStore = store;
13077
+ this.ownsArtifactStore = false;
13078
+ this.installOutputNormalizer(store);
13079
+ this.registerMemoryRetrievalTools();
13080
+ }
13081
+ /**
13082
+ * Build the store the `artifacts` config (or `STORAGE_TYPE`) asks for. The
13083
+ * Redis connection falls back to conversation memory's, so one
13084
+ * `STORAGE_TYPE=redis` moves sessions and artifacts together.
13085
+ */
13086
+ createConfiguredArtifactStore() {
13087
+ const injected = this.artifactsConfig?.store !== undefined;
13088
+ const store = createArtifactStore(this.artifactsConfig, this.conversationMemoryConfig?.conversationMemory?.redisConfig);
13089
+ // An injected store is returned as-is by the factory and stays the
13090
+ // caller's; anything else was built here and is this instance's to close.
13091
+ this.ownsArtifactStore = !injected;
13092
+ logger.debug("[NeuroLink] Artifact store initialized", {
13093
+ backend: injected
13094
+ ? "custom"
13095
+ : resolveArtifactStorageType(this.artifactsConfig),
13096
+ });
13097
+ return store;
13098
+ }
13099
+ /**
13100
+ * (Re)build the MCP output normalizer over `artifactStore`. A no-op unless
13101
+ * `mcp.outputLimits` was configured — without it nothing is externalized.
13102
+ */
13103
+ installOutputNormalizer(artifactStore) {
13104
+ if (!this.mcpOutputNormalizerConfig) {
13105
+ return;
13106
+ }
13107
+ this.externalServerManager.setOutputNormalizer(new McpOutputNormalizer(this.mcpOutputNormalizerConfig, artifactStore));
13108
+ logger.debug("[NeuroLink] MCP output normalizer initialized", {
13109
+ ...this.mcpOutputNormalizerConfig,
13110
+ hasArtifactStore: artifactStore !== undefined,
13111
+ });
13112
+ }
13020
13113
  /**
13021
13114
  * Bank a payload to a file and get back a pointer to it.
13022
13115
  *
@@ -13355,7 +13448,24 @@ Current user's request: ${currentInput}`;
13355
13448
  logger.warn("[NeuroLink] Error shutting down external MCP servers:", error);
13356
13449
  }
13357
13450
  }
13358
- // 3. Clear all event listeners to prevent memory leaks
13451
+ // 3. Release the artifact store this instance built (a pooled Redis
13452
+ // connection, for one). Same ownership rule as shutdown(): an injected
13453
+ // store is the caller's to close. Without this step every
13454
+ // construct / use / dispose cycle left the pooled reference acquired.
13455
+ if (this.ownsArtifactStore && this.mcpArtifactStore?.close) {
13456
+ try {
13457
+ await this.mcpArtifactStore.close();
13458
+ logger.debug("[NeuroLink] Artifact store closed");
13459
+ }
13460
+ catch (error) {
13461
+ const err = error instanceof Error
13462
+ ? error
13463
+ : new Error(`Artifact store close error: ${String(error)}`);
13464
+ cleanupErrors.push(err);
13465
+ logger.warn("[NeuroLink] Error closing artifact store:", error);
13466
+ }
13467
+ }
13468
+ // 4. Clear all event listeners to prevent memory leaks
13359
13469
  if (this.emitter) {
13360
13470
  try {
13361
13471
  logger.debug("[NeuroLink] Removing all event listeners...");
@@ -13373,7 +13483,7 @@ Current user's request: ${currentInput}`;
13373
13483
  logger.warn("[NeuroLink] Error removing event listeners:", error);
13374
13484
  }
13375
13485
  }
13376
- // 4. Clear all circuit breakers
13486
+ // 5. Clear all circuit breakers
13377
13487
  if (this.toolCircuitBreakers && this.toolCircuitBreakers.size > 0) {
13378
13488
  try {
13379
13489
  logger.debug(`[NeuroLink] Clearing ${this.toolCircuitBreakers.size} circuit breakers...`);
@@ -13388,7 +13498,7 @@ Current user's request: ${currentInput}`;
13388
13498
  logger.warn("[NeuroLink] Error clearing circuit breakers:", error);
13389
13499
  }
13390
13500
  }
13391
- // 5. Clear all Maps and caches
13501
+ // 6. Clear all Maps and caches
13392
13502
  try {
13393
13503
  logger.debug("[NeuroLink] Clearing maps and caches...");
13394
13504
  if (this.toolExecutionMetrics) {
@@ -13439,7 +13549,7 @@ Current user's request: ${currentInput}`;
13439
13549
  this._taskManager = undefined;
13440
13550
  }
13441
13551
  }
13442
- // 6. Reset initialization flags
13552
+ // 7. Reset initialization flags
13443
13553
  try {
13444
13554
  logger.debug("[NeuroLink] Resetting initialization state...");
13445
13555
  this.mcpInitialized = false;
@@ -13455,7 +13565,7 @@ Current user's request: ${currentInput}`;
13455
13565
  cleanupErrors.push(err);
13456
13566
  logger.warn("[NeuroLink] Error resetting state:", error);
13457
13567
  }
13458
- // 6. Log completion
13568
+ // 7. Log completion
13459
13569
  if (cleanupErrors.length === 0) {
13460
13570
  logger.debug("[NeuroLink] ✅ Resource disposal completed successfully");
13461
13571
  }