@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.
- package/CHANGELOG.md +8 -0
- package/README.md +7 -1
- package/dist/auth/anthropicOAuth.d.ts +18 -3
- package/dist/auth/anthropicOAuth.js +149 -4
- package/dist/auth/providers/firebase.js +5 -1
- package/dist/auth/providers/jwt.js +5 -1
- package/dist/auth/providers/workos.js +5 -1
- package/dist/auth/sessionManager.d.ts +1 -1
- package/dist/auth/sessionManager.js +58 -27
- package/dist/browser/neurolink.min.js +354 -334
- package/dist/cli/commands/mcp.d.ts +6 -0
- package/dist/cli/commands/mcp.js +188 -181
- package/dist/cli/commands/proxy.d.ts +2 -1
- package/dist/cli/commands/proxy.js +713 -431
- package/dist/cli/commands/task.js +3 -0
- package/dist/cli/factories/commandFactory.d.ts +2 -0
- package/dist/cli/factories/commandFactory.js +38 -0
- package/dist/cli/parser.js +4 -3
- package/dist/client/aiSdkAdapter.js +3 -0
- package/dist/client/streamingClient.js +30 -10
- package/dist/core/baseProvider.d.ts +6 -1
- package/dist/core/baseProvider.js +208 -230
- package/dist/core/factory.d.ts +3 -0
- package/dist/core/factory.js +138 -188
- package/dist/core/modules/GenerationHandler.js +3 -2
- package/dist/core/redisConversationMemoryManager.js +7 -3
- package/dist/evaluation/BatchEvaluator.js +4 -1
- package/dist/evaluation/hooks/observabilityHooks.js +5 -3
- package/dist/evaluation/pipeline/evaluationPipeline.d.ts +3 -2
- package/dist/evaluation/pipeline/evaluationPipeline.js +24 -9
- package/dist/evaluation/pipeline/strategies/batchStrategy.js +6 -3
- package/dist/evaluation/pipeline/strategies/samplingStrategy.js +18 -10
- package/dist/evaluation/scorers/scorerRegistry.d.ts +3 -0
- package/dist/evaluation/scorers/scorerRegistry.js +353 -282
- package/dist/lib/auth/anthropicOAuth.d.ts +18 -3
- package/dist/lib/auth/anthropicOAuth.js +149 -4
- package/dist/lib/auth/providers/firebase.js +5 -1
- package/dist/lib/auth/providers/jwt.js +5 -1
- package/dist/lib/auth/providers/workos.js +5 -1
- package/dist/lib/auth/sessionManager.d.ts +1 -1
- package/dist/lib/auth/sessionManager.js +58 -27
- package/dist/lib/client/aiSdkAdapter.js +3 -0
- package/dist/lib/client/streamingClient.js +30 -10
- package/dist/lib/core/baseProvider.d.ts +6 -1
- package/dist/lib/core/baseProvider.js +208 -230
- package/dist/lib/core/factory.d.ts +3 -0
- package/dist/lib/core/factory.js +138 -188
- package/dist/lib/core/modules/GenerationHandler.js +3 -2
- package/dist/lib/core/redisConversationMemoryManager.js +7 -3
- package/dist/lib/evaluation/BatchEvaluator.js +4 -1
- package/dist/lib/evaluation/hooks/observabilityHooks.js +5 -3
- package/dist/lib/evaluation/pipeline/evaluationPipeline.d.ts +3 -2
- package/dist/lib/evaluation/pipeline/evaluationPipeline.js +24 -9
- package/dist/lib/evaluation/pipeline/strategies/batchStrategy.js +6 -3
- package/dist/lib/evaluation/pipeline/strategies/samplingStrategy.js +18 -10
- package/dist/lib/evaluation/scorers/scorerRegistry.d.ts +3 -0
- package/dist/lib/evaluation/scorers/scorerRegistry.js +353 -282
- package/dist/lib/mcp/toolRegistry.d.ts +2 -0
- package/dist/lib/mcp/toolRegistry.js +32 -31
- package/dist/lib/neurolink.d.ts +41 -2
- package/dist/lib/neurolink.js +1616 -1681
- package/dist/lib/observability/otelBridge.d.ts +2 -2
- package/dist/lib/observability/otelBridge.js +12 -3
- package/dist/lib/providers/amazonBedrock.js +2 -4
- package/dist/lib/providers/anthropic.d.ts +9 -5
- package/dist/lib/providers/anthropic.js +19 -14
- package/dist/lib/providers/anthropicBaseProvider.d.ts +3 -3
- package/dist/lib/providers/anthropicBaseProvider.js +5 -4
- package/dist/lib/providers/azureOpenai.d.ts +1 -1
- package/dist/lib/providers/azureOpenai.js +5 -4
- package/dist/lib/providers/googleAiStudio.js +30 -6
- package/dist/lib/providers/googleVertex.d.ts +10 -0
- package/dist/lib/providers/googleVertex.js +437 -423
- package/dist/lib/providers/huggingFace.d.ts +3 -3
- package/dist/lib/providers/huggingFace.js +6 -8
- package/dist/lib/providers/litellm.d.ts +1 -0
- package/dist/lib/providers/litellm.js +76 -55
- package/dist/lib/providers/mistral.js +2 -1
- package/dist/lib/providers/ollama.js +93 -23
- package/dist/lib/providers/openAI.d.ts +2 -0
- package/dist/lib/providers/openAI.js +141 -141
- package/dist/lib/providers/openRouter.js +2 -1
- package/dist/lib/providers/openaiCompatible.d.ts +4 -4
- package/dist/lib/providers/openaiCompatible.js +4 -4
- package/dist/lib/proxy/claudeFormat.d.ts +3 -2
- package/dist/lib/proxy/claudeFormat.js +27 -14
- package/dist/lib/proxy/cloaking/plugins/sessionIdentity.d.ts +2 -6
- package/dist/lib/proxy/cloaking/plugins/sessionIdentity.js +9 -33
- package/dist/lib/proxy/modelRouter.js +3 -0
- package/dist/lib/proxy/oauthFetch.d.ts +1 -1
- package/dist/lib/proxy/oauthFetch.js +289 -316
- package/dist/lib/proxy/proxyConfig.js +46 -24
- package/dist/lib/proxy/proxyEnv.d.ts +19 -0
- package/dist/lib/proxy/proxyEnv.js +73 -0
- package/dist/lib/proxy/proxyFetch.js +291 -217
- package/dist/lib/proxy/proxyTracer.d.ts +133 -0
- package/dist/lib/proxy/proxyTracer.js +645 -0
- package/dist/lib/proxy/rawStreamCapture.d.ts +10 -0
- package/dist/lib/proxy/rawStreamCapture.js +83 -0
- package/dist/lib/proxy/requestLogger.d.ts +32 -5
- package/dist/lib/proxy/requestLogger.js +503 -47
- package/dist/lib/proxy/sseInterceptor.d.ts +97 -0
- package/dist/lib/proxy/sseInterceptor.js +427 -0
- package/dist/lib/proxy/usageStats.d.ts +4 -3
- package/dist/lib/proxy/usageStats.js +25 -12
- package/dist/lib/rag/chunkers/MarkdownChunker.js +13 -5
- package/dist/lib/rag/chunking/markdownChunker.js +15 -6
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +17 -3
- package/dist/lib/server/routes/claudeProxyRoutes.js +3032 -1349
- package/dist/lib/services/server/ai/observability/instrumentation.d.ts +7 -1
- package/dist/lib/services/server/ai/observability/instrumentation.js +337 -161
- package/dist/lib/tasks/backends/bullmqBackend.d.ts +1 -0
- package/dist/lib/tasks/backends/bullmqBackend.js +35 -22
- package/dist/lib/tasks/store/redisTaskStore.d.ts +1 -0
- package/dist/lib/tasks/store/redisTaskStore.js +54 -39
- package/dist/lib/tasks/taskManager.d.ts +5 -0
- package/dist/lib/tasks/taskManager.js +158 -30
- package/dist/lib/telemetry/index.d.ts +2 -1
- package/dist/lib/telemetry/index.js +2 -1
- package/dist/lib/telemetry/telemetryService.d.ts +3 -0
- package/dist/lib/telemetry/telemetryService.js +69 -5
- package/dist/lib/types/cli.d.ts +10 -0
- package/dist/lib/types/proxyTypes.d.ts +160 -5
- package/dist/lib/types/streamTypes.d.ts +25 -3
- package/dist/lib/utils/messageBuilder.js +3 -2
- package/dist/lib/utils/providerHealth.d.ts +19 -0
- package/dist/lib/utils/providerHealth.js +279 -33
- package/dist/lib/utils/providerUtils.js +17 -22
- package/dist/lib/utils/toolChoice.d.ts +4 -0
- package/dist/lib/utils/toolChoice.js +7 -0
- package/dist/mcp/toolRegistry.d.ts +2 -0
- package/dist/mcp/toolRegistry.js +32 -31
- package/dist/neurolink.d.ts +41 -2
- package/dist/neurolink.js +1616 -1681
- package/dist/observability/otelBridge.d.ts +2 -2
- package/dist/observability/otelBridge.js +12 -3
- package/dist/providers/amazonBedrock.js +2 -4
- package/dist/providers/anthropic.d.ts +9 -5
- package/dist/providers/anthropic.js +19 -14
- package/dist/providers/anthropicBaseProvider.d.ts +3 -3
- package/dist/providers/anthropicBaseProvider.js +5 -4
- package/dist/providers/azureOpenai.d.ts +1 -1
- package/dist/providers/azureOpenai.js +5 -4
- package/dist/providers/googleAiStudio.js +30 -6
- package/dist/providers/googleVertex.d.ts +10 -0
- package/dist/providers/googleVertex.js +437 -423
- package/dist/providers/huggingFace.d.ts +3 -3
- package/dist/providers/huggingFace.js +6 -7
- package/dist/providers/litellm.d.ts +1 -0
- package/dist/providers/litellm.js +76 -55
- package/dist/providers/mistral.js +2 -1
- package/dist/providers/ollama.js +93 -23
- package/dist/providers/openAI.d.ts +2 -0
- package/dist/providers/openAI.js +141 -141
- package/dist/providers/openRouter.js +2 -1
- package/dist/providers/openaiCompatible.d.ts +4 -4
- package/dist/providers/openaiCompatible.js +4 -3
- package/dist/proxy/claudeFormat.d.ts +3 -2
- package/dist/proxy/claudeFormat.js +27 -14
- package/dist/proxy/cloaking/plugins/sessionIdentity.d.ts +2 -6
- package/dist/proxy/cloaking/plugins/sessionIdentity.js +9 -33
- package/dist/proxy/modelRouter.js +3 -0
- package/dist/proxy/oauthFetch.d.ts +1 -1
- package/dist/proxy/oauthFetch.js +289 -316
- package/dist/proxy/proxyConfig.js +46 -24
- package/dist/proxy/proxyEnv.d.ts +19 -0
- package/dist/proxy/proxyEnv.js +72 -0
- package/dist/proxy/proxyFetch.js +291 -217
- package/dist/proxy/proxyTracer.d.ts +133 -0
- package/dist/proxy/proxyTracer.js +644 -0
- package/dist/proxy/rawStreamCapture.d.ts +10 -0
- package/dist/proxy/rawStreamCapture.js +82 -0
- package/dist/proxy/requestLogger.d.ts +32 -5
- package/dist/proxy/requestLogger.js +503 -47
- package/dist/proxy/sseInterceptor.d.ts +97 -0
- package/dist/proxy/sseInterceptor.js +426 -0
- package/dist/proxy/usageStats.d.ts +4 -3
- package/dist/proxy/usageStats.js +25 -12
- package/dist/rag/chunkers/MarkdownChunker.js +13 -5
- package/dist/rag/chunking/markdownChunker.js +15 -6
- package/dist/server/routes/claudeProxyRoutes.d.ts +17 -3
- package/dist/server/routes/claudeProxyRoutes.js +3032 -1349
- package/dist/services/server/ai/observability/instrumentation.d.ts +7 -1
- package/dist/services/server/ai/observability/instrumentation.js +337 -161
- package/dist/tasks/backends/bullmqBackend.d.ts +1 -0
- package/dist/tasks/backends/bullmqBackend.js +35 -22
- package/dist/tasks/store/redisTaskStore.d.ts +1 -0
- package/dist/tasks/store/redisTaskStore.js +54 -39
- package/dist/tasks/taskManager.d.ts +5 -0
- package/dist/tasks/taskManager.js +158 -30
- package/dist/telemetry/index.d.ts +2 -1
- package/dist/telemetry/index.js +2 -1
- package/dist/telemetry/telemetryService.d.ts +3 -0
- package/dist/telemetry/telemetryService.js +69 -5
- package/dist/types/cli.d.ts +10 -0
- package/dist/types/proxyTypes.d.ts +160 -5
- package/dist/types/streamTypes.d.ts +25 -3
- package/dist/utils/messageBuilder.js +3 -2
- package/dist/utils/providerHealth.d.ts +19 -0
- package/dist/utils/providerHealth.js +279 -33
- package/dist/utils/providerUtils.js +18 -22
- package/dist/utils/toolChoice.d.ts +4 -0
- package/dist/utils/toolChoice.js +6 -0
- package/docs/assets/dashboards/neurolink-proxy-observability-dashboard.json +6609 -0
- package/docs/changelog.md +252 -0
- package/package.json +19 -2
- package/scripts/observability/check-proxy-telemetry.mjs +235 -0
- package/scripts/observability/docker-compose.proxy-observability.yaml +55 -0
- package/scripts/observability/import-openobserve-dashboard.mjs +240 -0
- package/scripts/observability/manage-local-openobserve.sh +215 -0
- package/scripts/observability/otel-collector.proxy-observability.yaml +78 -0
- 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.
|
|
65
|
+
const queue = this.getQueue();
|
|
66
66
|
this.executors.set(task.id, executor);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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.
|
|
98
|
+
const queue = this.getQueue();
|
|
93
99
|
this.executors.delete(taskId);
|
|
94
100
|
// Remove repeatable job scheduler
|
|
95
101
|
try {
|
|
96
|
-
await
|
|
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
|
|
109
|
+
const job = await queue.getJob(taskId);
|
|
104
110
|
if (job) {
|
|
105
111
|
await job.remove();
|
|
106
112
|
}
|
|
@@ -185,4 +191,11 @@ 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
|
}
|
|
@@ -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.
|
|
65
|
-
await
|
|
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.
|
|
70
|
-
const data = await
|
|
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.
|
|
78
|
-
const all = await
|
|
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.
|
|
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
|
|
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.
|
|
102
|
+
const client = this.getClient();
|
|
103
103
|
await Promise.all([
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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.
|
|
111
|
+
const client = this.getClient();
|
|
112
112
|
const key = taskRunsKey(taskId);
|
|
113
|
-
await
|
|
113
|
+
await client.lPush(key, JSON.stringify(run));
|
|
114
114
|
// Trim to keep only the latest maxRunLogs entries
|
|
115
|
-
await
|
|
115
|
+
await client.lTrim(key, 0, this.maxRunLogs - 1);
|
|
116
116
|
}
|
|
117
117
|
async getRuns(taskId, options) {
|
|
118
|
-
this.
|
|
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
|
|
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.
|
|
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
|
|
138
|
+
await client.rPush(key, serialized);
|
|
139
139
|
// Trim to keep only the most recent entries, preventing unbounded growth
|
|
140
|
-
await
|
|
140
|
+
await client.lTrim(key, -this.maxHistoryEntries, -1);
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
async getHistory(taskId) {
|
|
144
|
-
this.
|
|
144
|
+
const client = this.getClient();
|
|
145
145
|
const key = taskHistoryKey(taskId);
|
|
146
|
-
const items = await
|
|
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.
|
|
151
|
-
await
|
|
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,19 +177,27 @@ export class RedisTaskStore {
|
|
|
170
177
|
cancelled: this.retentionConfig.cancelledTTL,
|
|
171
178
|
};
|
|
172
179
|
const ttlMs = ttlMap[task.status];
|
|
173
|
-
if (ttlMs) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
}
|
|
@@ -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
|
|
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
|
|
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
|
|
158
|
+
await backend.schedule(task, (t) => this.onTaskTick(t));
|
|
139
159
|
}
|
|
140
160
|
catch (err) {
|
|
141
|
-
|
|
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.
|
|
196
|
+
return this.getStore().get(taskId);
|
|
156
197
|
}
|
|
157
198
|
async list(filter) {
|
|
158
199
|
await this.ensureInitialized();
|
|
159
|
-
return this.
|
|
200
|
+
return this.getStore().list(filter);
|
|
160
201
|
}
|
|
161
202
|
async update(taskId, updates) {
|
|
162
203
|
await this.ensureInitialized();
|
|
163
|
-
const
|
|
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
|
|
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
|
-
|
|
204
|
-
await
|
|
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.
|
|
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
|
|
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
|
|
227
|
-
|
|
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
|
|
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
|
|
241
|
-
|
|
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
|
-
|
|
248
|
-
|
|
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.
|
|
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
|
|
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
|
|
424
|
+
const result = await executor.execute(current);
|
|
299
425
|
// Log the run
|
|
300
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
/**
|
package/dist/telemetry/index.js
CHANGED
|
@@ -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
|
-
*
|
|
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");
|