@juspay/neurolink 9.41.0 → 9.42.1

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 (212) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +7 -1
  3. package/dist/auth/anthropicOAuth.d.ts +18 -3
  4. package/dist/auth/anthropicOAuth.js +149 -4
  5. package/dist/auth/providers/firebase.js +5 -1
  6. package/dist/auth/providers/jwt.js +5 -1
  7. package/dist/auth/providers/workos.js +5 -1
  8. package/dist/auth/sessionManager.d.ts +1 -1
  9. package/dist/auth/sessionManager.js +58 -27
  10. package/dist/browser/neurolink.min.js +354 -334
  11. package/dist/cli/commands/mcp.d.ts +6 -0
  12. package/dist/cli/commands/mcp.js +188 -181
  13. package/dist/cli/commands/proxy.d.ts +2 -1
  14. package/dist/cli/commands/proxy.js +713 -431
  15. package/dist/cli/commands/task.js +3 -0
  16. package/dist/cli/factories/commandFactory.d.ts +2 -0
  17. package/dist/cli/factories/commandFactory.js +38 -0
  18. package/dist/cli/parser.js +4 -3
  19. package/dist/client/aiSdkAdapter.js +3 -0
  20. package/dist/client/streamingClient.js +30 -10
  21. package/dist/core/baseProvider.d.ts +6 -1
  22. package/dist/core/baseProvider.js +208 -230
  23. package/dist/core/factory.d.ts +3 -0
  24. package/dist/core/factory.js +138 -188
  25. package/dist/core/modules/GenerationHandler.js +3 -2
  26. package/dist/core/redisConversationMemoryManager.js +7 -3
  27. package/dist/evaluation/BatchEvaluator.js +4 -1
  28. package/dist/evaluation/hooks/observabilityHooks.js +5 -3
  29. package/dist/evaluation/pipeline/evaluationPipeline.d.ts +3 -2
  30. package/dist/evaluation/pipeline/evaluationPipeline.js +24 -9
  31. package/dist/evaluation/pipeline/strategies/batchStrategy.js +6 -3
  32. package/dist/evaluation/pipeline/strategies/samplingStrategy.js +18 -10
  33. package/dist/evaluation/scorers/scorerRegistry.d.ts +3 -0
  34. package/dist/evaluation/scorers/scorerRegistry.js +353 -282
  35. package/dist/lib/auth/anthropicOAuth.d.ts +18 -3
  36. package/dist/lib/auth/anthropicOAuth.js +149 -4
  37. package/dist/lib/auth/providers/firebase.js +5 -1
  38. package/dist/lib/auth/providers/jwt.js +5 -1
  39. package/dist/lib/auth/providers/workos.js +5 -1
  40. package/dist/lib/auth/sessionManager.d.ts +1 -1
  41. package/dist/lib/auth/sessionManager.js +58 -27
  42. package/dist/lib/client/aiSdkAdapter.js +3 -0
  43. package/dist/lib/client/streamingClient.js +30 -10
  44. package/dist/lib/core/baseProvider.d.ts +6 -1
  45. package/dist/lib/core/baseProvider.js +208 -230
  46. package/dist/lib/core/factory.d.ts +3 -0
  47. package/dist/lib/core/factory.js +138 -188
  48. package/dist/lib/core/modules/GenerationHandler.js +3 -2
  49. package/dist/lib/core/redisConversationMemoryManager.js +7 -3
  50. package/dist/lib/evaluation/BatchEvaluator.js +4 -1
  51. package/dist/lib/evaluation/hooks/observabilityHooks.js +5 -3
  52. package/dist/lib/evaluation/pipeline/evaluationPipeline.d.ts +3 -2
  53. package/dist/lib/evaluation/pipeline/evaluationPipeline.js +24 -9
  54. package/dist/lib/evaluation/pipeline/strategies/batchStrategy.js +6 -3
  55. package/dist/lib/evaluation/pipeline/strategies/samplingStrategy.js +18 -10
  56. package/dist/lib/evaluation/scorers/scorerRegistry.d.ts +3 -0
  57. package/dist/lib/evaluation/scorers/scorerRegistry.js +353 -282
  58. package/dist/lib/mcp/toolRegistry.d.ts +2 -0
  59. package/dist/lib/mcp/toolRegistry.js +32 -31
  60. package/dist/lib/neurolink.d.ts +41 -2
  61. package/dist/lib/neurolink.js +1616 -1681
  62. package/dist/lib/observability/otelBridge.d.ts +2 -2
  63. package/dist/lib/observability/otelBridge.js +12 -3
  64. package/dist/lib/providers/amazonBedrock.js +2 -4
  65. package/dist/lib/providers/anthropic.d.ts +9 -5
  66. package/dist/lib/providers/anthropic.js +19 -14
  67. package/dist/lib/providers/anthropicBaseProvider.d.ts +3 -3
  68. package/dist/lib/providers/anthropicBaseProvider.js +5 -4
  69. package/dist/lib/providers/azureOpenai.d.ts +1 -1
  70. package/dist/lib/providers/azureOpenai.js +5 -4
  71. package/dist/lib/providers/googleAiStudio.js +30 -6
  72. package/dist/lib/providers/googleVertex.d.ts +10 -0
  73. package/dist/lib/providers/googleVertex.js +437 -423
  74. package/dist/lib/providers/huggingFace.d.ts +3 -3
  75. package/dist/lib/providers/huggingFace.js +6 -8
  76. package/dist/lib/providers/litellm.d.ts +1 -0
  77. package/dist/lib/providers/litellm.js +76 -55
  78. package/dist/lib/providers/mistral.js +2 -1
  79. package/dist/lib/providers/ollama.js +93 -23
  80. package/dist/lib/providers/openAI.d.ts +2 -0
  81. package/dist/lib/providers/openAI.js +141 -141
  82. package/dist/lib/providers/openRouter.js +2 -1
  83. package/dist/lib/providers/openaiCompatible.d.ts +4 -4
  84. package/dist/lib/providers/openaiCompatible.js +4 -4
  85. package/dist/lib/proxy/claudeFormat.d.ts +3 -2
  86. package/dist/lib/proxy/claudeFormat.js +27 -14
  87. package/dist/lib/proxy/cloaking/plugins/sessionIdentity.d.ts +2 -6
  88. package/dist/lib/proxy/cloaking/plugins/sessionIdentity.js +9 -33
  89. package/dist/lib/proxy/modelRouter.js +3 -0
  90. package/dist/lib/proxy/oauthFetch.d.ts +1 -1
  91. package/dist/lib/proxy/oauthFetch.js +289 -316
  92. package/dist/lib/proxy/proxyConfig.js +46 -24
  93. package/dist/lib/proxy/proxyEnv.d.ts +19 -0
  94. package/dist/lib/proxy/proxyEnv.js +73 -0
  95. package/dist/lib/proxy/proxyFetch.js +291 -217
  96. package/dist/lib/proxy/proxyTracer.d.ts +133 -0
  97. package/dist/lib/proxy/proxyTracer.js +645 -0
  98. package/dist/lib/proxy/rawStreamCapture.d.ts +10 -0
  99. package/dist/lib/proxy/rawStreamCapture.js +83 -0
  100. package/dist/lib/proxy/requestLogger.d.ts +32 -5
  101. package/dist/lib/proxy/requestLogger.js +503 -47
  102. package/dist/lib/proxy/sseInterceptor.d.ts +97 -0
  103. package/dist/lib/proxy/sseInterceptor.js +427 -0
  104. package/dist/lib/proxy/usageStats.d.ts +4 -3
  105. package/dist/lib/proxy/usageStats.js +25 -12
  106. package/dist/lib/rag/chunkers/MarkdownChunker.js +13 -5
  107. package/dist/lib/rag/chunking/markdownChunker.js +15 -6
  108. package/dist/lib/server/routes/claudeProxyRoutes.d.ts +17 -3
  109. package/dist/lib/server/routes/claudeProxyRoutes.js +3032 -1349
  110. package/dist/lib/services/server/ai/observability/instrumentation.d.ts +7 -1
  111. package/dist/lib/services/server/ai/observability/instrumentation.js +337 -161
  112. package/dist/lib/tasks/backends/bullmqBackend.d.ts +1 -0
  113. package/dist/lib/tasks/backends/bullmqBackend.js +35 -22
  114. package/dist/lib/tasks/store/redisTaskStore.d.ts +1 -0
  115. package/dist/lib/tasks/store/redisTaskStore.js +54 -39
  116. package/dist/lib/tasks/taskManager.d.ts +5 -0
  117. package/dist/lib/tasks/taskManager.js +158 -30
  118. package/dist/lib/telemetry/index.d.ts +2 -1
  119. package/dist/lib/telemetry/index.js +2 -1
  120. package/dist/lib/telemetry/telemetryService.d.ts +3 -0
  121. package/dist/lib/telemetry/telemetryService.js +69 -5
  122. package/dist/lib/types/cli.d.ts +10 -0
  123. package/dist/lib/types/proxyTypes.d.ts +160 -5
  124. package/dist/lib/types/streamTypes.d.ts +25 -3
  125. package/dist/lib/utils/messageBuilder.js +3 -2
  126. package/dist/lib/utils/providerHealth.d.ts +19 -0
  127. package/dist/lib/utils/providerHealth.js +279 -33
  128. package/dist/lib/utils/providerUtils.js +17 -22
  129. package/dist/lib/utils/toolChoice.d.ts +4 -0
  130. package/dist/lib/utils/toolChoice.js +7 -0
  131. package/dist/mcp/toolRegistry.d.ts +2 -0
  132. package/dist/mcp/toolRegistry.js +32 -31
  133. package/dist/neurolink.d.ts +41 -2
  134. package/dist/neurolink.js +1616 -1681
  135. package/dist/observability/otelBridge.d.ts +2 -2
  136. package/dist/observability/otelBridge.js +12 -3
  137. package/dist/providers/amazonBedrock.js +2 -4
  138. package/dist/providers/anthropic.d.ts +9 -5
  139. package/dist/providers/anthropic.js +19 -14
  140. package/dist/providers/anthropicBaseProvider.d.ts +3 -3
  141. package/dist/providers/anthropicBaseProvider.js +5 -4
  142. package/dist/providers/azureOpenai.d.ts +1 -1
  143. package/dist/providers/azureOpenai.js +5 -4
  144. package/dist/providers/googleAiStudio.js +30 -6
  145. package/dist/providers/googleVertex.d.ts +10 -0
  146. package/dist/providers/googleVertex.js +437 -423
  147. package/dist/providers/huggingFace.d.ts +3 -3
  148. package/dist/providers/huggingFace.js +6 -7
  149. package/dist/providers/litellm.d.ts +1 -0
  150. package/dist/providers/litellm.js +76 -55
  151. package/dist/providers/mistral.js +2 -1
  152. package/dist/providers/ollama.js +93 -23
  153. package/dist/providers/openAI.d.ts +2 -0
  154. package/dist/providers/openAI.js +141 -141
  155. package/dist/providers/openRouter.js +2 -1
  156. package/dist/providers/openaiCompatible.d.ts +4 -4
  157. package/dist/providers/openaiCompatible.js +4 -3
  158. package/dist/proxy/claudeFormat.d.ts +3 -2
  159. package/dist/proxy/claudeFormat.js +27 -14
  160. package/dist/proxy/cloaking/plugins/sessionIdentity.d.ts +2 -6
  161. package/dist/proxy/cloaking/plugins/sessionIdentity.js +9 -33
  162. package/dist/proxy/modelRouter.js +3 -0
  163. package/dist/proxy/oauthFetch.d.ts +1 -1
  164. package/dist/proxy/oauthFetch.js +289 -316
  165. package/dist/proxy/proxyConfig.js +46 -24
  166. package/dist/proxy/proxyEnv.d.ts +19 -0
  167. package/dist/proxy/proxyEnv.js +72 -0
  168. package/dist/proxy/proxyFetch.js +291 -217
  169. package/dist/proxy/proxyTracer.d.ts +133 -0
  170. package/dist/proxy/proxyTracer.js +644 -0
  171. package/dist/proxy/rawStreamCapture.d.ts +10 -0
  172. package/dist/proxy/rawStreamCapture.js +82 -0
  173. package/dist/proxy/requestLogger.d.ts +32 -5
  174. package/dist/proxy/requestLogger.js +503 -47
  175. package/dist/proxy/sseInterceptor.d.ts +97 -0
  176. package/dist/proxy/sseInterceptor.js +426 -0
  177. package/dist/proxy/usageStats.d.ts +4 -3
  178. package/dist/proxy/usageStats.js +25 -12
  179. package/dist/rag/chunkers/MarkdownChunker.js +13 -5
  180. package/dist/rag/chunking/markdownChunker.js +15 -6
  181. package/dist/server/routes/claudeProxyRoutes.d.ts +17 -3
  182. package/dist/server/routes/claudeProxyRoutes.js +3032 -1349
  183. package/dist/services/server/ai/observability/instrumentation.d.ts +7 -1
  184. package/dist/services/server/ai/observability/instrumentation.js +337 -161
  185. package/dist/tasks/backends/bullmqBackend.d.ts +1 -0
  186. package/dist/tasks/backends/bullmqBackend.js +35 -22
  187. package/dist/tasks/store/redisTaskStore.d.ts +1 -0
  188. package/dist/tasks/store/redisTaskStore.js +54 -39
  189. package/dist/tasks/taskManager.d.ts +5 -0
  190. package/dist/tasks/taskManager.js +158 -30
  191. package/dist/telemetry/index.d.ts +2 -1
  192. package/dist/telemetry/index.js +2 -1
  193. package/dist/telemetry/telemetryService.d.ts +3 -0
  194. package/dist/telemetry/telemetryService.js +69 -5
  195. package/dist/types/cli.d.ts +10 -0
  196. package/dist/types/proxyTypes.d.ts +160 -5
  197. package/dist/types/streamTypes.d.ts +25 -3
  198. package/dist/utils/messageBuilder.js +3 -2
  199. package/dist/utils/providerHealth.d.ts +19 -0
  200. package/dist/utils/providerHealth.js +279 -33
  201. package/dist/utils/providerUtils.js +18 -22
  202. package/dist/utils/toolChoice.d.ts +4 -0
  203. package/dist/utils/toolChoice.js +6 -0
  204. package/docs/assets/dashboards/neurolink-proxy-observability-dashboard.json +6609 -0
  205. package/docs/changelog.md +252 -0
  206. package/package.json +19 -2
  207. package/scripts/observability/check-proxy-telemetry.mjs +235 -0
  208. package/scripts/observability/docker-compose.proxy-observability.yaml +55 -0
  209. package/scripts/observability/import-openobserve-dashboard.mjs +240 -0
  210. package/scripts/observability/manage-local-openobserve.sh +215 -0
  211. package/scripts/observability/otel-collector.proxy-observability.yaml +78 -0
  212. package/scripts/observability/proxy-observability.env.example +23 -0
@@ -62,45 +62,51 @@ export class BullMQBackend {
62
62
  logger.info("[BullMQ] Backend shut down");
63
63
  }
64
64
  async schedule(task, executor) {
65
- this.ensureInitialized();
65
+ const queue = this.getQueue();
66
66
  this.executors.set(task.id, executor);
67
- const jobData = { taskId: task.id, task };
68
- const schedule = task.schedule;
69
- if (schedule.type === "cron") {
70
- await this.queue.upsertJobScheduler(task.id, {
71
- pattern: schedule.expression,
72
- ...(schedule.timezone ? { tz: schedule.timezone } : {}),
73
- }, { name: task.name, data: jobData });
74
- }
75
- else if (schedule.type === "interval") {
76
- await this.queue.upsertJobScheduler(task.id, { every: schedule.every }, { name: task.name, data: jobData });
67
+ try {
68
+ const jobData = { taskId: task.id, task };
69
+ const schedule = task.schedule;
70
+ if (schedule.type === "cron") {
71
+ await queue.upsertJobScheduler(task.id, {
72
+ pattern: schedule.expression,
73
+ ...(schedule.timezone ? { tz: schedule.timezone } : {}),
74
+ }, { name: task.name, data: jobData });
75
+ }
76
+ else if (schedule.type === "interval") {
77
+ await queue.upsertJobScheduler(task.id, { every: schedule.every }, { name: task.name, data: jobData });
78
+ }
79
+ else if (schedule.type === "once") {
80
+ const at = typeof schedule.at === "string" ? new Date(schedule.at) : schedule.at;
81
+ const delay = Math.max(0, at.getTime() - Date.now());
82
+ await queue.add(task.name, jobData, {
83
+ jobId: task.id,
84
+ delay,
85
+ });
86
+ }
77
87
  }
78
- else if (schedule.type === "once") {
79
- const at = typeof schedule.at === "string" ? new Date(schedule.at) : schedule.at;
80
- const delay = Math.max(0, at.getTime() - Date.now());
81
- await this.queue.add(task.name, jobData, {
82
- jobId: task.id,
83
- delay,
84
- });
88
+ catch (error) {
89
+ this.executors.delete(task.id);
90
+ throw error;
85
91
  }
86
92
  logger.info("[BullMQ] Task scheduled", {
87
93
  taskId: task.id,
88
- type: schedule.type,
94
+ type: task.schedule.type,
89
95
  });
90
96
  }
91
97
  async cancel(taskId) {
92
- this.ensureInitialized();
98
+ const queue = this.getQueue();
93
99
  this.executors.delete(taskId);
94
100
  // Remove repeatable job scheduler
95
101
  try {
96
- await this.queue.removeJobScheduler(taskId);
102
+ await queue.removeJobScheduler(taskId);
97
103
  }
98
104
  catch {
99
105
  // May not be a repeatable job — try removing by job ID
100
106
  }
101
107
  // Remove delayed/waiting job
102
108
  try {
103
- const job = await this.queue.getJob(taskId);
109
+ const job = await queue.getJob(taskId);
104
110
  if (job) {
105
111
  await job.remove();
106
112
  }
@@ -185,5 +191,12 @@ export class BullMQBackend {
185
191
  throw TaskError.create("BACKEND_NOT_INITIALIZED", "[BullMQ] Backend not initialized. Call initialize() first.");
186
192
  }
187
193
  }
194
+ getQueue() {
195
+ this.ensureInitialized();
196
+ if (!this.queue) {
197
+ throw TaskError.create("BACKEND_NOT_INITIALIZED", "[BullMQ] Queue is unavailable after initialization.");
198
+ }
199
+ return this.queue;
200
+ }
188
201
  }
189
202
  //# sourceMappingURL=bullmqBackend.js.map
@@ -34,6 +34,7 @@ export declare class RedisTaskStore implements TaskStore {
34
34
  getHistory(taskId: string): Promise<ConversationEntry[]>;
35
35
  clearHistory(taskId: string): Promise<void>;
36
36
  private ensureConnected;
37
+ private getClient;
37
38
  /**
38
39
  * Set Redis TTL on terminal-state tasks so they auto-expire.
39
40
  * Active and paused tasks never expire.
@@ -61,21 +61,21 @@ export class RedisTaskStore {
61
61
  }
62
62
  // ── Task CRUD ───────────────────────────────────────────
63
63
  async save(task) {
64
- this.ensureConnected();
65
- await this.client.hSet(TASKS_HASH, task.id, JSON.stringify(task));
66
- this.applyRetentionTTL(task);
64
+ const client = this.getClient();
65
+ await client.hSet(TASKS_HASH, task.id, JSON.stringify(task));
66
+ this.applyRetentionTTL(task, client);
67
67
  }
68
68
  async get(taskId) {
69
- this.ensureConnected();
70
- const data = await this.client.hGet(TASKS_HASH, taskId);
69
+ const client = this.getClient();
70
+ const data = await client.hGet(TASKS_HASH, taskId);
71
71
  if (!data) {
72
72
  return null;
73
73
  }
74
74
  return JSON.parse(String(data));
75
75
  }
76
76
  async list(filter) {
77
- this.ensureConnected();
78
- const all = await this.client.hGetAll(TASKS_HASH);
77
+ const client = this.getClient();
78
+ const all = await client.hGetAll(TASKS_HASH);
79
79
  let tasks = Object.values(all).map((v) => JSON.parse(String(v)));
80
80
  if (filter?.status) {
81
81
  tasks = tasks.filter((t) => t.status === filter.status);
@@ -83,7 +83,7 @@ export class RedisTaskStore {
83
83
  return tasks.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
84
84
  }
85
85
  async update(taskId, updates) {
86
- this.ensureConnected();
86
+ const client = this.getClient();
87
87
  const existing = await this.get(taskId);
88
88
  if (!existing) {
89
89
  throw TaskError.create("TASK_NOT_FOUND", `Task not found: ${taskId}`);
@@ -94,35 +94,35 @@ export class RedisTaskStore {
94
94
  id: existing.id, // ID is immutable
95
95
  updatedAt: new Date().toISOString(),
96
96
  };
97
- await this.client.hSet(TASKS_HASH, taskId, JSON.stringify(updated));
98
- this.applyRetentionTTL(updated);
97
+ await client.hSet(TASKS_HASH, taskId, JSON.stringify(updated));
98
+ this.applyRetentionTTL(updated, client);
99
99
  return updated;
100
100
  }
101
101
  async delete(taskId) {
102
- this.ensureConnected();
102
+ const client = this.getClient();
103
103
  await Promise.all([
104
- this.client.hDel(TASKS_HASH, taskId),
105
- this.client.del(taskRunsKey(taskId)),
106
- this.client.del(taskHistoryKey(taskId)),
104
+ client.hDel(TASKS_HASH, taskId),
105
+ client.del(taskRunsKey(taskId)),
106
+ client.del(taskHistoryKey(taskId)),
107
107
  ]);
108
108
  }
109
109
  // ── Run Logs ──────────────────────────────────────────
110
110
  async appendRun(taskId, run) {
111
- this.ensureConnected();
111
+ const client = this.getClient();
112
112
  const key = taskRunsKey(taskId);
113
- await this.client.lPush(key, JSON.stringify(run));
113
+ await client.lPush(key, JSON.stringify(run));
114
114
  // Trim to keep only the latest maxRunLogs entries
115
- await this.client.lTrim(key, 0, this.maxRunLogs - 1);
115
+ await client.lTrim(key, 0, this.maxRunLogs - 1);
116
116
  }
117
117
  async getRuns(taskId, options) {
118
- this.ensureConnected();
118
+ const client = this.getClient();
119
119
  const limit = options?.limit ?? 20;
120
120
  const key = taskRunsKey(taskId);
121
121
  // When a status filter is applied, we need to fetch more items than `limit`
122
122
  // because post-filter may discard many entries. Fetch all (-1) when filtering,
123
123
  // otherwise fetch exactly `limit` items.
124
124
  const fetchEnd = options?.status ? -1 : limit - 1;
125
- const items = await this.client.lRange(key, 0, fetchEnd);
125
+ const items = await client.lRange(key, 0, fetchEnd);
126
126
  let runs = items.map((v) => JSON.parse(String(v)));
127
127
  if (options?.status) {
128
128
  runs = runs.filter((r) => r.status === options.status);
@@ -131,24 +131,24 @@ export class RedisTaskStore {
131
131
  }
132
132
  // ── Continuation History ──────────────────────────────
133
133
  async appendHistory(taskId, messages) {
134
- this.ensureConnected();
134
+ const client = this.getClient();
135
135
  const key = taskHistoryKey(taskId);
136
136
  const serialized = messages.map((m) => JSON.stringify(m));
137
137
  if (serialized.length > 0) {
138
- await this.client.rPush(key, serialized);
138
+ await client.rPush(key, serialized);
139
139
  // Trim to keep only the most recent entries, preventing unbounded growth
140
- await this.client.lTrim(key, -this.maxHistoryEntries, -1);
140
+ await client.lTrim(key, -this.maxHistoryEntries, -1);
141
141
  }
142
142
  }
143
143
  async getHistory(taskId) {
144
- this.ensureConnected();
144
+ const client = this.getClient();
145
145
  const key = taskHistoryKey(taskId);
146
- const items = await this.client.lRange(key, 0, -1);
146
+ const items = await client.lRange(key, 0, -1);
147
147
  return items.map((v) => JSON.parse(String(v)));
148
148
  }
149
149
  async clearHistory(taskId) {
150
- this.ensureConnected();
151
- await this.client.del(taskHistoryKey(taskId));
150
+ const client = this.getClient();
151
+ await client.del(taskHistoryKey(taskId));
152
152
  }
153
153
  // ── Internal ──────────────────────────────────────────
154
154
  ensureConnected() {
@@ -156,11 +156,18 @@ export class RedisTaskStore {
156
156
  throw TaskError.create("BACKEND_NOT_INITIALIZED", "[TaskStore:Redis] Not connected. Call initialize() first.");
157
157
  }
158
158
  }
159
+ getClient() {
160
+ this.ensureConnected();
161
+ if (!this.client) {
162
+ throw TaskError.create("BACKEND_NOT_INITIALIZED", "[TaskStore:Redis] Client is unavailable after initialization.");
163
+ }
164
+ return this.client;
165
+ }
159
166
  /**
160
167
  * Set Redis TTL on terminal-state tasks so they auto-expire.
161
168
  * Active and paused tasks never expire.
162
169
  */
163
- applyRetentionTTL(task) {
170
+ applyRetentionTTL(task, client) {
164
171
  // We don't set EXPIRE on the hash field directly (Redis doesn't support per-field TTL).
165
172
  // Instead, run logs and history keys get TTL. The task hash field itself must be
166
173
  // cleaned up via manual deletion or BullMQ's built-in job cleanup.
@@ -170,20 +177,28 @@ export class RedisTaskStore {
170
177
  cancelled: this.retentionConfig.cancelledTTL,
171
178
  };
172
179
  const ttlMs = ttlMap[task.status];
173
- if (ttlMs) {
174
- const ttlSeconds = Math.ceil(ttlMs / 1000);
175
- // Set TTL on associated keys
176
- this.client.expire(taskRunsKey(task.id), ttlSeconds).catch((err) => {
177
- logger.debug("[TaskStore:Redis] Failed to set TTL", {
178
- error: String(err),
179
- });
180
+ if (!ttlMs || !client.isOpen) {
181
+ return;
182
+ }
183
+ const ttlSeconds = Math.ceil(ttlMs / 1000);
184
+ // Set TTL on associated keys best-effort. A successful task write should not
185
+ // be surfaced as a failure just because the retention metadata could not be updated.
186
+ client.expire(taskRunsKey(task.id), ttlSeconds).catch((err) => {
187
+ logger.warn("[TaskStore:Redis] Failed to set TTL on task runs key — task data may outlive retention window", {
188
+ taskId: task.id,
189
+ key: taskRunsKey(task.id),
190
+ ttlSeconds,
191
+ error: String(err),
180
192
  });
181
- this.client.expire(taskHistoryKey(task.id), ttlSeconds).catch((err) => {
182
- logger.debug("[TaskStore:Redis] Failed to set TTL", {
183
- error: String(err),
184
- });
193
+ });
194
+ client.expire(taskHistoryKey(task.id), ttlSeconds).catch((err) => {
195
+ logger.warn("[TaskStore:Redis] Failed to set TTL on task history key — task data may outlive retention window", {
196
+ taskId: task.id,
197
+ key: taskHistoryKey(task.id),
198
+ ttlSeconds,
199
+ error: String(err),
185
200
  });
186
- }
201
+ });
187
202
  }
188
203
  }
189
204
  //# sourceMappingURL=redisTaskStore.js.map
@@ -28,6 +28,9 @@ export declare class TaskManager {
28
28
  }): void;
29
29
  private ensureInitialized;
30
30
  private doInitialize;
31
+ private getStore;
32
+ private getBackend;
33
+ private getExecutor;
31
34
  create(definition: TaskDefinition): Promise<Task>;
32
35
  get(taskId: string): Promise<Task | null>;
33
36
  list(filter?: {
@@ -46,6 +49,8 @@ export declare class TaskManager {
46
49
  shutdown(): Promise<void>;
47
50
  /** Check if the backend is healthy */
48
51
  isHealthy(): Promise<boolean>;
52
+ private restoreScheduledTask;
53
+ private rollbackTaskUpdate;
49
54
  /**
50
55
  * Called by the backend on each scheduled tick.
51
56
  * Executes the task, updates state, fires callbacks/events.
@@ -70,15 +70,35 @@ export class TaskManager {
70
70
  store: this.store.type,
71
71
  });
72
72
  }
73
+ getStore() {
74
+ if (!this.store) {
75
+ throw TaskError.create("BACKEND_NOT_INITIALIZED", "[TaskManager] Store not initialized. Call initialize() first.");
76
+ }
77
+ return this.store;
78
+ }
79
+ getBackend() {
80
+ if (!this.backend) {
81
+ throw TaskError.create("BACKEND_NOT_INITIALIZED", "[TaskManager] Backend not initialized. Call initialize() first.");
82
+ }
83
+ return this.backend;
84
+ }
85
+ getExecutor() {
86
+ if (!this.executor) {
87
+ throw TaskError.create("BACKEND_NOT_INITIALIZED", "[TaskManager] Executor not initialized. Call initialize() first.");
88
+ }
89
+ return this.executor;
90
+ }
73
91
  // ── Public API ────────────────────────────────────────
74
92
  async create(definition) {
75
93
  if (this.config.enabled === false) {
76
94
  throw TaskError.create("TASK_DISABLED", "TaskManager is disabled. Set tasks.enabled to true in config.");
77
95
  }
78
96
  await this.ensureInitialized();
97
+ const store = this.getStore();
98
+ const backend = this.getBackend();
79
99
  // Enforce maximum task limit to prevent unbounded task creation
80
100
  const maxTasks = this.config.maxTasks ?? TASK_DEFAULTS.maxTasks;
81
- const existingTasks = await this.store.list();
101
+ const existingTasks = await store.list();
82
102
  if (existingTasks.length >= maxTasks) {
83
103
  throw TaskError.create("TASK_LIMIT_REACHED", `Task limit reached (${maxTasks}). Delete existing tasks or increase maxTasks config.`);
84
104
  }
@@ -124,7 +144,7 @@ export class TaskManager {
124
144
  task.sessionId = `session_${nanoid(12)}`;
125
145
  }
126
146
  // Save to store
127
- await this.store.save(task);
147
+ await store.save(task);
128
148
  // Register callbacks (in-memory only)
129
149
  if (definition.onSuccess || definition.onError || definition.onComplete) {
130
150
  this.callbacks.set(task.id, {
@@ -135,10 +155,31 @@ export class TaskManager {
135
155
  }
136
156
  // Schedule
137
157
  try {
138
- await this.backend.schedule(task, (t) => this.onTaskTick(t));
158
+ await backend.schedule(task, (t) => this.onTaskTick(t));
139
159
  }
140
160
  catch (err) {
141
- await this.store.delete(task.id);
161
+ this.callbacks.delete(task.id);
162
+ try {
163
+ await store.delete(task.id);
164
+ }
165
+ catch (cleanupError) {
166
+ // Deletion failed — task remains persisted as active. Attempt to mark it
167
+ // failed so it reaches a terminal state and operators can identify it.
168
+ logger.error("[TaskManager] Failed to clean up task after schedule error — task may remain persisted as active", {
169
+ taskId: task.id,
170
+ scheduleError: String(err),
171
+ cleanupError: String(cleanupError),
172
+ });
173
+ try {
174
+ await store.update(task.id, { status: "failed" });
175
+ }
176
+ catch (terminalError) {
177
+ logger.error("[TaskManager] Failed to force task to terminal state — manual cleanup required", {
178
+ taskId: task.id,
179
+ error: String(terminalError),
180
+ });
181
+ }
182
+ }
142
183
  throw err;
143
184
  }
144
185
  this.emit("task:created", task);
@@ -152,15 +193,17 @@ export class TaskManager {
152
193
  }
153
194
  async get(taskId) {
154
195
  await this.ensureInitialized();
155
- return this.store.get(taskId);
196
+ return this.getStore().get(taskId);
156
197
  }
157
198
  async list(filter) {
158
199
  await this.ensureInitialized();
159
- return this.store.list(filter);
200
+ return this.getStore().list(filter);
160
201
  }
161
202
  async update(taskId, updates) {
162
203
  await this.ensureInitialized();
163
- const existing = await this.store.get(taskId);
204
+ const store = this.getStore();
205
+ const backend = this.getBackend();
206
+ const existing = await store.get(taskId);
164
207
  if (!existing) {
165
208
  throw TaskError.create("TASK_NOT_FOUND", `Task not found: ${taskId}`);
166
209
  }
@@ -187,6 +230,7 @@ export class TaskManager {
187
230
  taskUpdates[field] = updates[field];
188
231
  }
189
232
  }
233
+ const shouldClearHistory = updates.mode !== undefined && updates.mode !== "continuation";
190
234
  // Special-case: mode changes require sessionId handling
191
235
  if (updates.mode !== undefined) {
192
236
  if (updates.mode === "continuation" && !existing.sessionId) {
@@ -194,21 +238,46 @@ export class TaskManager {
194
238
  }
195
239
  else if (updates.mode !== "continuation") {
196
240
  taskUpdates.sessionId = undefined;
197
- await this.store.clearHistory(taskId);
198
241
  }
199
242
  }
200
- const updated = await this.store.update(taskId, taskUpdates);
243
+ const updated = await store.update(taskId, taskUpdates);
201
244
  // Re-schedule if schedule changed and task is active
202
245
  if (updates.schedule && updated.status === "active") {
203
- await this.backend.cancel(taskId);
204
- await this.backend.schedule(updated, (t) => this.onTaskTick(t));
246
+ const attemptedSchedule = updated.schedule;
247
+ await backend.cancel(taskId);
248
+ try {
249
+ await backend.schedule(updated, (t) => this.onTaskTick(t));
250
+ }
251
+ catch (error) {
252
+ await this.restoreScheduledTask(existing, "update schedule rollback");
253
+ await this.rollbackTaskUpdate(taskId, existing, error);
254
+ throw TaskError.create("SCHEDULE_FAILED", `Failed to update schedule for task ${taskId}`, {
255
+ cause: error instanceof Error ? error : undefined,
256
+ details: {
257
+ taskId,
258
+ previousSchedule: existing.schedule,
259
+ attemptedSchedule,
260
+ },
261
+ });
262
+ }
263
+ }
264
+ if (shouldClearHistory) {
265
+ try {
266
+ await store.clearHistory(taskId);
267
+ }
268
+ catch (error) {
269
+ logger.warn("[TaskManager] Failed to clear task history after mode update", {
270
+ taskId,
271
+ error: String(error),
272
+ });
273
+ }
205
274
  }
206
275
  return updated;
207
276
  }
208
277
  /** Run a task immediately (outside of its schedule) */
209
278
  async run(taskId) {
210
279
  await this.ensureInitialized();
211
- const task = await this.store.get(taskId);
280
+ const task = await this.getStore().get(taskId);
212
281
  if (!task) {
213
282
  throw TaskError.create("TASK_NOT_FOUND", `Task not found: ${taskId}`);
214
283
  }
@@ -216,42 +285,64 @@ export class TaskManager {
216
285
  }
217
286
  async pause(taskId) {
218
287
  await this.ensureInitialized();
219
- const task = await this.store.get(taskId);
288
+ const store = this.getStore();
289
+ const backend = this.getBackend();
290
+ const task = await store.get(taskId);
220
291
  if (!task) {
221
292
  throw TaskError.create("TASK_NOT_FOUND", `Task not found: ${taskId}`);
222
293
  }
223
294
  if (task.status !== "active") {
224
295
  throw TaskError.create("INVALID_TASK_STATUS", `Cannot pause task with status: ${task.status}`);
225
296
  }
226
- await this.backend.pause(taskId);
227
- const updated = await this.store.update(taskId, { status: "paused" });
297
+ await backend.pause(taskId);
298
+ let updated;
299
+ try {
300
+ updated = await store.update(taskId, { status: "paused" });
301
+ }
302
+ catch (error) {
303
+ await this.restoreScheduledTask(task, "pause rollback");
304
+ throw error;
305
+ }
228
306
  this.emit("task:paused", updated);
229
307
  return updated;
230
308
  }
231
309
  async resume(taskId) {
232
310
  await this.ensureInitialized();
233
- const task = await this.store.get(taskId);
311
+ const store = this.getStore();
312
+ const backend = this.getBackend();
313
+ const task = await store.get(taskId);
234
314
  if (!task) {
235
315
  throw TaskError.create("TASK_NOT_FOUND", `Task not found: ${taskId}`);
236
316
  }
237
317
  if (task.status !== "paused") {
238
318
  throw TaskError.create("INVALID_TASK_STATUS", `Cannot resume task with status: ${task.status}`);
239
319
  }
240
- const updated = await this.store.update(taskId, { status: "active" });
241
- await this.backend.schedule(updated, (t) => this.onTaskTick(t));
320
+ const updated = await store.update(taskId, { status: "active" });
321
+ try {
322
+ await backend.schedule(updated, (t) => this.onTaskTick(t));
323
+ }
324
+ catch (error) {
325
+ await this.rollbackTaskUpdate(taskId, task, error);
326
+ throw TaskError.create("SCHEDULE_FAILED", `Failed to resume task ${taskId}`, {
327
+ cause: error instanceof Error ? error : undefined,
328
+ details: { taskId, schedule: task.schedule },
329
+ });
330
+ }
242
331
  this.emit("task:resumed", updated);
243
332
  return updated;
244
333
  }
245
334
  async delete(taskId) {
246
335
  await this.ensureInitialized();
247
- await this.backend.cancel(taskId);
248
- await this.store.delete(taskId);
336
+ const backend = this.getBackend();
337
+ const store = this.getStore();
338
+ await backend.cancel(taskId);
339
+ await store.delete(taskId);
249
340
  this.callbacks.delete(taskId);
250
341
  this.emit("task:deleted", taskId);
251
342
  }
252
343
  async runs(taskId, options) {
253
344
  await this.ensureInitialized();
254
- return this.store.getRuns(taskId, options);
345
+ return this.getStore().getRuns(taskId, options);
255
346
  }
256
347
  async shutdown() {
257
348
  if (this.backend) {
@@ -273,14 +364,49 @@ export class TaskManager {
273
364
  return this.backend.isHealthy();
274
365
  }
275
366
  // ── Internal ──────────────────────────────────────────
367
+ async restoreScheduledTask(task, reason) {
368
+ if (task.status !== "active") {
369
+ return;
370
+ }
371
+ try {
372
+ await this.getBackend().schedule(task, (t) => this.onTaskTick(t));
373
+ logger.warn("[TaskManager] Restored task schedule after rollback", {
374
+ taskId: task.id,
375
+ reason,
376
+ });
377
+ }
378
+ catch (restoreError) {
379
+ logger.error("[TaskManager] Failed to restore task schedule during rollback", {
380
+ taskId: task.id,
381
+ reason,
382
+ error: String(restoreError),
383
+ });
384
+ }
385
+ }
386
+ async rollbackTaskUpdate(taskId, previousTask, error) {
387
+ try {
388
+ return await this.getStore().update(taskId, previousTask);
389
+ }
390
+ catch (rollbackError) {
391
+ logger.error("[TaskManager] Failed to roll back task update — store and in-memory state may be diverged; manual reconciliation required", {
392
+ taskId,
393
+ originalError: String(error),
394
+ rollbackError: String(rollbackError),
395
+ });
396
+ throw rollbackError;
397
+ }
398
+ }
276
399
  /**
277
400
  * Called by the backend on each scheduled tick.
278
401
  * Executes the task, updates state, fires callbacks/events.
279
402
  */
280
403
  async onTaskTick(task) {
281
404
  this.emit("task:started", task);
405
+ const store = this.getStore();
406
+ const backend = this.getBackend();
407
+ const executor = this.getExecutor();
282
408
  // Re-read latest task state (may have been updated/paused since scheduling)
283
- const current = await this.store.get(task.id);
409
+ const current = await store.get(task.id);
284
410
  if (!current || current.status !== "active") {
285
411
  logger.debug("[TaskManager] Skipping tick for non-active task", {
286
412
  taskId: task.id,
@@ -295,9 +421,9 @@ export class TaskManager {
295
421
  timestamp: new Date().toISOString(),
296
422
  };
297
423
  }
298
- const result = await this.executor.execute(current);
424
+ const result = await executor.execute(current);
299
425
  // Log the run
300
- await this.store.appendRun(task.id, result);
426
+ await store.appendRun(task.id, result);
301
427
  // Update task tracking
302
428
  const updates = {
303
429
  runCount: current.runCount + 1,
@@ -306,18 +432,18 @@ export class TaskManager {
306
432
  // Check if task should complete
307
433
  if (current.maxRuns && current.runCount + 1 >= current.maxRuns) {
308
434
  updates.status = "completed";
309
- await this.backend.cancel(task.id);
435
+ await backend.cancel(task.id);
310
436
  }
311
437
  // Mark successful once tasks as completed
312
438
  if (result.status === "success" && current.schedule.type === "once") {
313
439
  updates.status = "completed";
314
- await this.backend.cancel(task.id);
440
+ await backend.cancel(task.id);
315
441
  }
316
442
  // Mark as failed on permanent error
317
443
  if (result.status === "error" && current.schedule.type === "once") {
318
444
  updates.status = "failed";
319
445
  }
320
- await this.store.update(task.id, updates);
446
+ await store.update(task.id, updates);
321
447
  // Fire callbacks
322
448
  const cbs = this.callbacks.get(task.id);
323
449
  if (cbs) {
@@ -337,7 +463,7 @@ export class TaskManager {
337
463
  });
338
464
  }
339
465
  if (updates.status === "completed" || updates.status === "failed") {
340
- const finalTask = await this.store.get(task.id);
466
+ const finalTask = await store.get(task.id);
341
467
  if (finalTask && cbs.onComplete) {
342
468
  await cbs.onComplete(finalTask);
343
469
  }
@@ -364,10 +490,12 @@ export class TaskManager {
364
490
  * Called on initialization to handle process restarts.
365
491
  */
366
492
  async rescheduleActiveTasks() {
367
- const activeTasks = await this.store.list({ status: "active" });
493
+ const store = this.getStore();
494
+ const backend = this.getBackend();
495
+ const activeTasks = await store.list({ status: "active" });
368
496
  for (const task of activeTasks) {
369
497
  try {
370
- await this.backend.schedule(task, (t) => this.onTaskTick(t));
498
+ await backend.schedule(task, (t) => this.onTaskTick(t));
371
499
  logger.debug("[TaskManager] Re-scheduled task", {
372
500
  taskId: task.id,
373
501
  name: task.name,
@@ -4,7 +4,8 @@ export { withSpan, withClientSpan, type SpanOptions } from "./withSpan.js";
4
4
  export { ATTR } from "./attributes.js";
5
5
  /**
6
6
  * Initialize telemetry for NeuroLink
7
- * OPTIONAL - Only works when NEUROLINK_TELEMETRY_ENABLED=true
7
+ * Reuses an existing global TracerProvider when one is already registered,
8
+ * otherwise bootstraps Neurolink telemetry when an exporter endpoint is configured.
8
9
  */
9
10
  export declare function initializeTelemetry(): Promise<import("./telemetryService.js").TelemetryService>;
10
11
  /**
@@ -6,7 +6,8 @@ export { ATTR } from "./attributes.js";
6
6
  import { logger } from "../utils/logger.js";
7
7
  /**
8
8
  * Initialize telemetry for NeuroLink
9
- * OPTIONAL - Only works when NEUROLINK_TELEMETRY_ENABLED=true
9
+ * Reuses an existing global TracerProvider when one is already registered,
10
+ * otherwise bootstraps Neurolink telemetry when an exporter endpoint is configured.
10
11
  */
11
12
  export async function initializeTelemetry() {
12
13
  const { TelemetryService } = await import("./telemetryService.js");