@juspay/neurolink 11.25.2 → 11.25.4
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 +5 -3
- package/dist/browser/neurolink.min.js +350 -350
- package/dist/core/baseProvider.d.ts +50 -4
- package/dist/core/baseProvider.js +117 -5
- package/dist/localUsage/claudeCodeReader.js +2 -6
- package/dist/localUsage/codexReader.js +2 -6
- package/dist/localUsage/openCodeReader.js +4 -6
- package/dist/localUsage/scanWindow.d.ts +29 -0
- package/dist/localUsage/scanWindow.js +42 -0
- package/dist/providers/anthropic/client.js +22 -1
- package/package.json +1 -1
|
@@ -4,10 +4,6 @@ import type { NeuroLink } from "../neurolink.js";
|
|
|
4
4
|
import type { UnknownRecord, MiddlewareFactoryOptions, StreamOptions, StreamResult, AIProvider, AnalyticsData, EnhancedGenerateResult, TextGenerationOptions, TextGenerationResult, ValidationSchema } from "../types/index.js";
|
|
5
5
|
import { TelemetryHandler } from "./modules/TelemetryHandler.js";
|
|
6
6
|
import type { LanguageModel, ModelMessage, Tool, ToolCallRepairFunction, ToolSet } from "../types/index.js";
|
|
7
|
-
/**
|
|
8
|
-
* Abstract base class for all AI providers
|
|
9
|
-
* Tools are integrated as first-class citizens - always available by default
|
|
10
|
-
*/
|
|
11
7
|
export declare abstract class BaseProvider implements AIProvider {
|
|
12
8
|
protected modelName: string;
|
|
13
9
|
protected readonly providerName: AIProviderName;
|
|
@@ -387,6 +383,56 @@ export declare abstract class BaseProvider implements AIProvider {
|
|
|
387
383
|
* original identity so that isAbortError() can detect them in
|
|
388
384
|
* retry/fallback loops (directProviderGeneration, performMCPGenerationRetries).
|
|
389
385
|
*/
|
|
386
|
+
/**
|
|
387
|
+
* Classify an error that escaped while the consumer was ITERATING a stream.
|
|
388
|
+
*
|
|
389
|
+
* `stream()` only awaits the CONSTRUCTION of the provider's stream object, and a provider
|
|
390
|
+
* that discovers its failure lazily throws on first pull instead — so the raw upstream
|
|
391
|
+
* error reached the consumer with no provider tag and no classification, while the same
|
|
392
|
+
* failure through `generate()` was classified normally. Measured on OpenAI:
|
|
393
|
+
*
|
|
394
|
+
* streaming "You have no credits remaining."
|
|
395
|
+
* non-streaming "[openai] OpenAI quota exhausted — this will not resolve by retrying..."
|
|
396
|
+
*
|
|
397
|
+
* Three guards. Only the third is load-bearing against today's code; the first two are
|
|
398
|
+
* deliberate depth against a hazard that is real but not currently reachable. Measured,
|
|
399
|
+
* rather than assumed — see the note after the list.
|
|
400
|
+
*
|
|
401
|
+
* 1. ALREADY STAMPED — everything that went through `handleProviderError` carries the mark,
|
|
402
|
+
* including the two formatters whose results escape the ProviderError hierarchy
|
|
403
|
+
* (`amazonSagemaker` returns SageMakerError, `replicate` returns NeuroLinkError; both
|
|
404
|
+
* extend Error directly). Without this they would be classified a second time and
|
|
405
|
+
* degraded.
|
|
406
|
+
* 2. ALREADY A ProviderError — covers providers that call `formatProviderError` DIRECTLY,
|
|
407
|
+
* bypassing `handleProviderError` and therefore the stamp. Anthropic does this in its
|
|
408
|
+
* own streaming catch (`anthropic/client.ts`), and its result is a ProviderError.
|
|
409
|
+
* 3. HAS AN HTTP STATUS — without this, an ordinary bug is relabelled as a provider
|
|
410
|
+
* failure. `classifyProviderError` ends in an unconditional catch-all
|
|
411
|
+
* (`utils/errorClassifier.ts`: `if (!rule) return new ProviderError(...)`) that is not
|
|
412
|
+
* gated on the error having come off the wire. Measured with the guard absent:
|
|
413
|
+
* TypeError "Cannot read properties of undefined (reading 'content')"
|
|
414
|
+
* became ProviderError "[openai] openai error: Cannot read properties of undefined..."
|
|
415
|
+
* which would hide a real defect behind a plausible provider message.
|
|
416
|
+
*
|
|
417
|
+
* WHY 1 AND 2 ARE NOT CURRENTLY REACHABLE, and why they stay anyway. A statusCode is
|
|
418
|
+
* attached to a formatted error in exactly one place — `handleProviderError` below — and
|
|
419
|
+
* that same method applies the stamp. So a ProviderError carrying a status is always
|
|
420
|
+
* stamped (caught by guard 1's own condition), and a ProviderError produced by a direct
|
|
421
|
+
* `formatProviderError` call carries no status, so guard 3 already returns it untouched.
|
|
422
|
+
* Verified by disabling guards 1 and 2 together, rebuilding, and re-running: the mocked
|
|
423
|
+
* provider contract suite stayed 64/64 and a mocked Anthropic streaming 429 was
|
|
424
|
+
* byte-identical (`RateLimitError`, one `[anthropic]` prefix).
|
|
425
|
+
*
|
|
426
|
+
* They remain because the hazard they cover is measured and real: `handleProviderError`
|
|
427
|
+
* is NOT idempotent — it copies statusCode onto its own output, so a second pass
|
|
428
|
+
* re-matches the bare 429 rule and DEGRADES the classification:
|
|
429
|
+
* pass 1 ProviderError "...quota exhausted — this will not resolve by retrying..."
|
|
430
|
+
* pass 2 RateLimitError "...rate limit exceeded..."
|
|
431
|
+
* The day any provider attaches a status to an error it formats itself, guard 3 stops
|
|
432
|
+
* covering that case and this degradation becomes live. Cheap insurance, not dead code —
|
|
433
|
+
* but do not cite guards 1 and 2 as proven-by-failure the way guard 3 is.
|
|
434
|
+
*/
|
|
435
|
+
protected classifyStreamError(error: unknown): Error;
|
|
390
436
|
protected handleProviderError(error: unknown): Error;
|
|
391
437
|
/**
|
|
392
438
|
* Image generation method. Providers that support it should override this.
|
|
@@ -5,6 +5,7 @@ import { MiddlewareFactory } from "../middleware/factory.js";
|
|
|
5
5
|
import { modelSupports } from "../models/modelRegistry.js";
|
|
6
6
|
import { ATTR, tracers } from "../telemetry/index.js";
|
|
7
7
|
import { ERROR_CODES, isAbortError, NeuroLinkError, } from "../utils/errorHandling.js";
|
|
8
|
+
import { ProviderError } from "../types/index.js";
|
|
8
9
|
import { sanitizeErrorCause } from "../utils/logSanitize.js";
|
|
9
10
|
import { createAnalytics as buildAnalytics } from "./analytics.js";
|
|
10
11
|
import { ErrorCategory, ErrorSeverity } from "../constants/enums.js";
|
|
@@ -48,6 +49,47 @@ function getLifecycleMiddlewareConfig(options) {
|
|
|
48
49
|
* Abstract base class for all AI providers
|
|
49
50
|
* Tools are integrated as first-class citizens - always available by default
|
|
50
51
|
*/
|
|
52
|
+
/**
|
|
53
|
+
* Marks an error as already run through `formatProviderError`.
|
|
54
|
+
*
|
|
55
|
+
* `handleProviderError` is NOT idempotent — measured: feeding its own output back in
|
|
56
|
+
* degrades a specific classification into a generic one, because it copies `statusCode`
|
|
57
|
+
* onto the formatted error, so a second pass re-matches the bare 429 rule while the more
|
|
58
|
+
* specific rule (which keyed on the raw body) no longer can:
|
|
59
|
+
*
|
|
60
|
+
* pass 1 ProviderError "[openai] OpenAI quota exhausted — this will not resolve by retrying..."
|
|
61
|
+
* pass 2 RateLimitError "[openai] OpenAI rate limit exceeded. Please try again later."
|
|
62
|
+
*
|
|
63
|
+
* Since the stream path can now reach a classifier that other paths already reached, that
|
|
64
|
+
* second pass became possible and had to be made impossible.
|
|
65
|
+
*
|
|
66
|
+
* Same `Symbol.for` stamping technique as `utils/lifecycleCallbacks.ts` and for the same
|
|
67
|
+
* reason: it survives across module copies where a closed-over WeakSet would not, and a
|
|
68
|
+
* frozen error degrades to one extra classification rather than a throw.
|
|
69
|
+
*/
|
|
70
|
+
const PROVIDER_ERROR_CLASSIFIED = Symbol.for("neurolink.providerErrorClassified");
|
|
71
|
+
function markProviderErrorClassified(error) {
|
|
72
|
+
if (error === null || typeof error !== "object") {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
Object.defineProperty(error, PROVIDER_ERROR_CLASSIFIED, {
|
|
77
|
+
value: true,
|
|
78
|
+
enumerable: false,
|
|
79
|
+
writable: false,
|
|
80
|
+
configurable: false,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Non-extensible error — worst case is one redundant classification.
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function isProviderErrorClassified(error) {
|
|
88
|
+
if (error === null || typeof error !== "object") {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
return error[PROVIDER_ERROR_CLASSIFIED] === true;
|
|
92
|
+
}
|
|
51
93
|
export class BaseProvider {
|
|
52
94
|
// Not `readonly` because providers that auto-discover the model from a
|
|
53
95
|
// /v1/models endpoint (lm-studio, llamacpp) need to update modelName after
|
|
@@ -307,10 +349,14 @@ export class BaseProvider {
|
|
|
307
349
|
*/
|
|
308
350
|
wrapStreamWithLifecycleCallbacks(result, options) {
|
|
309
351
|
const lifecycle = getLifecycleMiddlewareConfig(options);
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
352
|
+
// No early return when there are no callbacks. This wrapper is the only point
|
|
353
|
+
// every provider's stream passes through unconditionally (the real-streaming path
|
|
354
|
+
// plus all three fake-streaming paths), and returning `result` untouched here is
|
|
355
|
+
// exactly why a provider error that surfaces during ITERATION reached the consumer
|
|
356
|
+
// unclassified: the object handed back was the provider's own generator, by
|
|
357
|
+
// reference, and every layer below is a proven passthrough. The callbacks below are
|
|
358
|
+
// each individually guarded, so with none registered this only adds the catch.
|
|
359
|
+
const { onChunk, onFinish, onError } = lifecycle ?? {};
|
|
314
360
|
const startTime = Date.now();
|
|
315
361
|
const originalStream = result.stream;
|
|
316
362
|
// Lifecycle callbacks are awaited with a bounded deadline so callers
|
|
@@ -334,6 +380,9 @@ export class BaseProvider {
|
|
|
334
380
|
logger.warn(`[lifecycle] ${label} callback error:`, e);
|
|
335
381
|
}
|
|
336
382
|
};
|
|
383
|
+
// Arrow, like `safeFire` above: the generator is a plain function expression, so
|
|
384
|
+
// `this` is not bound inside it.
|
|
385
|
+
const classifyStreamError = (e) => this.classifyStreamError(e);
|
|
337
386
|
const wrappedStream = (async function* () {
|
|
338
387
|
let accumulated = "";
|
|
339
388
|
let seq = 0;
|
|
@@ -382,7 +431,7 @@ export class BaseProvider {
|
|
|
382
431
|
recoverable: false,
|
|
383
432
|
}), "onError");
|
|
384
433
|
}
|
|
385
|
-
throw err;
|
|
434
|
+
throw classifyStreamError(err);
|
|
386
435
|
}
|
|
387
436
|
})();
|
|
388
437
|
return { ...result, stream: wrappedStream };
|
|
@@ -1573,6 +1622,65 @@ export class BaseProvider {
|
|
|
1573
1622
|
* original identity so that isAbortError() can detect them in
|
|
1574
1623
|
* retry/fallback loops (directProviderGeneration, performMCPGenerationRetries).
|
|
1575
1624
|
*/
|
|
1625
|
+
/**
|
|
1626
|
+
* Classify an error that escaped while the consumer was ITERATING a stream.
|
|
1627
|
+
*
|
|
1628
|
+
* `stream()` only awaits the CONSTRUCTION of the provider's stream object, and a provider
|
|
1629
|
+
* that discovers its failure lazily throws on first pull instead — so the raw upstream
|
|
1630
|
+
* error reached the consumer with no provider tag and no classification, while the same
|
|
1631
|
+
* failure through `generate()` was classified normally. Measured on OpenAI:
|
|
1632
|
+
*
|
|
1633
|
+
* streaming "You have no credits remaining."
|
|
1634
|
+
* non-streaming "[openai] OpenAI quota exhausted — this will not resolve by retrying..."
|
|
1635
|
+
*
|
|
1636
|
+
* Three guards. Only the third is load-bearing against today's code; the first two are
|
|
1637
|
+
* deliberate depth against a hazard that is real but not currently reachable. Measured,
|
|
1638
|
+
* rather than assumed — see the note after the list.
|
|
1639
|
+
*
|
|
1640
|
+
* 1. ALREADY STAMPED — everything that went through `handleProviderError` carries the mark,
|
|
1641
|
+
* including the two formatters whose results escape the ProviderError hierarchy
|
|
1642
|
+
* (`amazonSagemaker` returns SageMakerError, `replicate` returns NeuroLinkError; both
|
|
1643
|
+
* extend Error directly). Without this they would be classified a second time and
|
|
1644
|
+
* degraded.
|
|
1645
|
+
* 2. ALREADY A ProviderError — covers providers that call `formatProviderError` DIRECTLY,
|
|
1646
|
+
* bypassing `handleProviderError` and therefore the stamp. Anthropic does this in its
|
|
1647
|
+
* own streaming catch (`anthropic/client.ts`), and its result is a ProviderError.
|
|
1648
|
+
* 3. HAS AN HTTP STATUS — without this, an ordinary bug is relabelled as a provider
|
|
1649
|
+
* failure. `classifyProviderError` ends in an unconditional catch-all
|
|
1650
|
+
* (`utils/errorClassifier.ts`: `if (!rule) return new ProviderError(...)`) that is not
|
|
1651
|
+
* gated on the error having come off the wire. Measured with the guard absent:
|
|
1652
|
+
* TypeError "Cannot read properties of undefined (reading 'content')"
|
|
1653
|
+
* became ProviderError "[openai] openai error: Cannot read properties of undefined..."
|
|
1654
|
+
* which would hide a real defect behind a plausible provider message.
|
|
1655
|
+
*
|
|
1656
|
+
* WHY 1 AND 2 ARE NOT CURRENTLY REACHABLE, and why they stay anyway. A statusCode is
|
|
1657
|
+
* attached to a formatted error in exactly one place — `handleProviderError` below — and
|
|
1658
|
+
* that same method applies the stamp. So a ProviderError carrying a status is always
|
|
1659
|
+
* stamped (caught by guard 1's own condition), and a ProviderError produced by a direct
|
|
1660
|
+
* `formatProviderError` call carries no status, so guard 3 already returns it untouched.
|
|
1661
|
+
* Verified by disabling guards 1 and 2 together, rebuilding, and re-running: the mocked
|
|
1662
|
+
* provider contract suite stayed 64/64 and a mocked Anthropic streaming 429 was
|
|
1663
|
+
* byte-identical (`RateLimitError`, one `[anthropic]` prefix).
|
|
1664
|
+
*
|
|
1665
|
+
* They remain because the hazard they cover is measured and real: `handleProviderError`
|
|
1666
|
+
* is NOT idempotent — it copies statusCode onto its own output, so a second pass
|
|
1667
|
+
* re-matches the bare 429 rule and DEGRADES the classification:
|
|
1668
|
+
* pass 1 ProviderError "...quota exhausted — this will not resolve by retrying..."
|
|
1669
|
+
* pass 2 RateLimitError "...rate limit exceeded..."
|
|
1670
|
+
* The day any provider attaches a status to an error it formats itself, guard 3 stops
|
|
1671
|
+
* covering that case and this degradation becomes live. Cheap insurance, not dead code —
|
|
1672
|
+
* but do not cite guards 1 and 2 as proven-by-failure the way guard 3 is.
|
|
1673
|
+
*/
|
|
1674
|
+
classifyStreamError(error) {
|
|
1675
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
1676
|
+
if (isProviderErrorClassified(err) || err instanceof ProviderError) {
|
|
1677
|
+
return err;
|
|
1678
|
+
}
|
|
1679
|
+
if (duckTypedStatusCode(err) === undefined) {
|
|
1680
|
+
return err;
|
|
1681
|
+
}
|
|
1682
|
+
return this.handleProviderError(err);
|
|
1683
|
+
}
|
|
1576
1684
|
handleProviderError(error) {
|
|
1577
1685
|
if (isAbortError(error)) {
|
|
1578
1686
|
// Preserve AbortError identity — never wrap in provider-specific formatting
|
|
@@ -1644,6 +1752,10 @@ export class BaseProvider {
|
|
|
1644
1752
|
catch {
|
|
1645
1753
|
// Non-blocking — telemetry failures shouldn't mask the original error
|
|
1646
1754
|
}
|
|
1755
|
+
// Stamp AFTER formatting so any later catch site can tell this error has
|
|
1756
|
+
// already been classified. Deliberately not applied to the AbortError
|
|
1757
|
+
// passthrough above — that returns the original error untouched.
|
|
1758
|
+
markProviderErrorClassified(formatted);
|
|
1647
1759
|
return formatted;
|
|
1648
1760
|
}
|
|
1649
1761
|
/**
|
|
@@ -27,8 +27,8 @@ import { readdir, stat } from "fs/promises";
|
|
|
27
27
|
import { homedir } from "os";
|
|
28
28
|
import { join } from "path";
|
|
29
29
|
import { calculateCost, hasPricing } from "../utils/pricing.js";
|
|
30
|
+
import { resolveScanCutoffMs } from "./scanWindow.js";
|
|
30
31
|
const CLI_ID = "claude-code";
|
|
31
|
-
const DEFAULT_SINCE_DAYS = 30;
|
|
32
32
|
const PROVIDER = "anthropic";
|
|
33
33
|
function projectsRoot() {
|
|
34
34
|
return join(homedir(), ".claude", "projects");
|
|
@@ -190,11 +190,7 @@ export async function createClaudeCodeReader() {
|
|
|
190
190
|
// 32.8s — the same unbounded sweep this guard exists to prevent, reached
|
|
191
191
|
// by a different door. The old guard caught it with Number.isFinite and
|
|
192
192
|
// the replacement dropped that check.
|
|
193
|
-
const
|
|
194
|
-
const sinceDays = Number.isNaN(requestedDays) ? 0 : requestedDays;
|
|
195
|
-
const cutoff = sinceDays === Infinity
|
|
196
|
-
? undefined
|
|
197
|
-
: Date.now() - Math.max(0, sinceDays) * 86_400_000;
|
|
193
|
+
const cutoff = resolveScanCutoffMs(options?.sinceDays);
|
|
198
194
|
let filesScanned = 0;
|
|
199
195
|
for (const file of files) {
|
|
200
196
|
try {
|
|
@@ -31,8 +31,8 @@ import { createInterface } from "readline";
|
|
|
31
31
|
import { readdir, stat } from "fs/promises";
|
|
32
32
|
import { homedir } from "os";
|
|
33
33
|
import { join } from "path";
|
|
34
|
+
import { resolveScanCutoffMs } from "./scanWindow.js";
|
|
34
35
|
const CLI_ID = "codex";
|
|
35
|
-
const DEFAULT_SINCE_DAYS = 30;
|
|
36
36
|
function sessionsRoot() {
|
|
37
37
|
return join(homedir(), ".codex", "sessions");
|
|
38
38
|
}
|
|
@@ -178,11 +178,7 @@ export async function createCodexReader() {
|
|
|
178
178
|
// 32.8s — the same unbounded sweep this guard exists to prevent, reached
|
|
179
179
|
// by a different door. The old guard caught it with Number.isFinite and
|
|
180
180
|
// the replacement dropped that check.
|
|
181
|
-
const
|
|
182
|
-
const sinceDays = Number.isNaN(requestedDays) ? 0 : requestedDays;
|
|
183
|
-
const cutoff = sinceDays === Infinity
|
|
184
|
-
? undefined
|
|
185
|
-
: Date.now() - Math.max(0, sinceDays) * 86_400_000;
|
|
181
|
+
const cutoff = resolveScanCutoffMs(options?.sinceDays);
|
|
186
182
|
let filesScanned = 0;
|
|
187
183
|
for (const file of files) {
|
|
188
184
|
try {
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
import { stat } from "fs/promises";
|
|
36
36
|
import { homedir } from "os";
|
|
37
37
|
import { join } from "path";
|
|
38
|
+
import { resolveScanCutoffMs } from "./scanWindow.js";
|
|
38
39
|
const CLI_ID = "opencode";
|
|
39
|
-
const DEFAULT_SINCE_DAYS = 30;
|
|
40
40
|
function databasePath() {
|
|
41
41
|
return join(homedir(), ".local", "share", "opencode", "opencode.db");
|
|
42
42
|
}
|
|
@@ -131,11 +131,9 @@ export async function createOpenCodeReader() {
|
|
|
131
131
|
// "no such column: NaN" — so the whole scan failed rather than
|
|
132
132
|
// over-reading. The cutoff is a bound parameter now, so a value can
|
|
133
133
|
// never be SQL syntax whatever it is.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const cutoffMs = sinceDays
|
|
137
|
-
? 0
|
|
138
|
-
: Date.now() - Math.max(0, sinceDays) * 86_400_000;
|
|
134
|
+
// `?? 0` because this value becomes a SQL parameter: an unbounded scan
|
|
135
|
+
// is expressed as "since the epoch", never as a non-finite number.
|
|
136
|
+
const cutoffMs = resolveScanCutoffMs(options?.sinceDays) ?? 0;
|
|
139
137
|
let db;
|
|
140
138
|
try {
|
|
141
139
|
// Read-only: this is the user's live store and OpenCode may be running.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One definition of "how far back does a local-usage scan reach".
|
|
3
|
+
*
|
|
4
|
+
* This calculation lived in three copies — claudeCodeReader, codexReader and
|
|
5
|
+
* openCodeReader — and has been wrong three separate times, each a different
|
|
6
|
+
* door into the same failure: an unbounded sweep of every transcript on the
|
|
7
|
+
* machine in answer to the narrowest possible request.
|
|
8
|
+
*
|
|
9
|
+
* sinceDays: 0 left the cutoff undefined 17,534 files, 35.9s
|
|
10
|
+
* sinceDays: NaN Math.max(0, NaN) is NaN, and every comparison
|
|
11
|
+
* against NaN is false, so the filter passed
|
|
12
|
+
* everything 17,537 files, 32.8s
|
|
13
|
+
* sinceDays: MAX_VALUE MAX_VALUE * 86_400_000 overflows to Infinity, so the
|
|
14
|
+
* cutoff became -Infinity and every file passed
|
|
15
|
+
*
|
|
16
|
+
* Three fixes in three files each closed one door and left the others open.
|
|
17
|
+
* The logic lives here once now, so the next door closes everywhere at once.
|
|
18
|
+
*
|
|
19
|
+
* Contract: `Infinity` is the ONLY value meaning all-history. Anything that is
|
|
20
|
+
* not a usable finite window — NaN, negative, or so large the arithmetic stops
|
|
21
|
+
* being finite — collapses to a zero-length window, which is what a
|
|
22
|
+
* nonsensical request should read as.
|
|
23
|
+
*/
|
|
24
|
+
export declare const DEFAULT_SINCE_DAYS = 30;
|
|
25
|
+
/**
|
|
26
|
+
* @returns the epoch-ms cutoff a scan must not read past, or `undefined` for an
|
|
27
|
+
* unbounded (all-history) scan — which only `Infinity` produces.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveScanCutoffMs(sinceDays: number | undefined, defaultDays?: number): number | undefined;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One definition of "how far back does a local-usage scan reach".
|
|
3
|
+
*
|
|
4
|
+
* This calculation lived in three copies — claudeCodeReader, codexReader and
|
|
5
|
+
* openCodeReader — and has been wrong three separate times, each a different
|
|
6
|
+
* door into the same failure: an unbounded sweep of every transcript on the
|
|
7
|
+
* machine in answer to the narrowest possible request.
|
|
8
|
+
*
|
|
9
|
+
* sinceDays: 0 left the cutoff undefined 17,534 files, 35.9s
|
|
10
|
+
* sinceDays: NaN Math.max(0, NaN) is NaN, and every comparison
|
|
11
|
+
* against NaN is false, so the filter passed
|
|
12
|
+
* everything 17,537 files, 32.8s
|
|
13
|
+
* sinceDays: MAX_VALUE MAX_VALUE * 86_400_000 overflows to Infinity, so the
|
|
14
|
+
* cutoff became -Infinity and every file passed
|
|
15
|
+
*
|
|
16
|
+
* Three fixes in three files each closed one door and left the others open.
|
|
17
|
+
* The logic lives here once now, so the next door closes everywhere at once.
|
|
18
|
+
*
|
|
19
|
+
* Contract: `Infinity` is the ONLY value meaning all-history. Anything that is
|
|
20
|
+
* not a usable finite window — NaN, negative, or so large the arithmetic stops
|
|
21
|
+
* being finite — collapses to a zero-length window, which is what a
|
|
22
|
+
* nonsensical request should read as.
|
|
23
|
+
*/
|
|
24
|
+
export const DEFAULT_SINCE_DAYS = 30;
|
|
25
|
+
const MS_PER_DAY = 86_400_000;
|
|
26
|
+
/**
|
|
27
|
+
* @returns the epoch-ms cutoff a scan must not read past, or `undefined` for an
|
|
28
|
+
* unbounded (all-history) scan — which only `Infinity` produces.
|
|
29
|
+
*/
|
|
30
|
+
export function resolveScanCutoffMs(sinceDays, defaultDays = DEFAULT_SINCE_DAYS) {
|
|
31
|
+
const requested = sinceDays ?? defaultDays;
|
|
32
|
+
const days = Number.isNaN(requested) ? 0 : requested;
|
|
33
|
+
if (days === Infinity) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
const span = Math.max(0, days) * MS_PER_DAY;
|
|
37
|
+
// A finite `days` can still produce a non-finite span: Number.MAX_VALUE and
|
|
38
|
+
// anything near it overflow on the multiply. Treat that as no window rather
|
|
39
|
+
// than letting `Date.now() - Infinity` become -Infinity, which every
|
|
40
|
+
// timestamp compares as newer than.
|
|
41
|
+
return Date.now() - (Number.isFinite(span) ? span : 0);
|
|
42
|
+
}
|
|
@@ -1738,7 +1738,28 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
1738
1738
|
}
|
|
1739
1739
|
}
|
|
1740
1740
|
})();
|
|
1741
|
-
|
|
1741
|
+
// `pump` is detached: it starts draining the engine's channel the moment
|
|
1742
|
+
// it is created, and `await pump` below is the only thing that adopts its
|
|
1743
|
+
// rejection. When resultPromise rejects, that line is never reached, so
|
|
1744
|
+
// pump's rejection stays unhandled — and an unhandled rejection
|
|
1745
|
+
// TERMINATES the consumer's process. Measured: a caller that correctly
|
|
1746
|
+
// try/catches a streaming error still died with ERR_UNHANDLED_REJECTION,
|
|
1747
|
+
// exit code 1, with no way to defend against it from outside this
|
|
1748
|
+
// library. The rejection carried the raw SDK error, distinct from the
|
|
1749
|
+
// formatted one the caller received, which is why the existing
|
|
1750
|
+
// `loopPromise.catch` guard below does not cover it.
|
|
1751
|
+
//
|
|
1752
|
+
// Same shape googleAiStudio/client.ts and googleVertex/client.ts already
|
|
1753
|
+
// use at every one of their pump sites; Anthropic was the only provider
|
|
1754
|
+
// missing it.
|
|
1755
|
+
let result;
|
|
1756
|
+
try {
|
|
1757
|
+
result = await resultPromise;
|
|
1758
|
+
}
|
|
1759
|
+
catch (error) {
|
|
1760
|
+
await pump.catch(() => { });
|
|
1761
|
+
throw error;
|
|
1762
|
+
}
|
|
1742
1763
|
await pump;
|
|
1743
1764
|
totalInput += result.usage.inputTokens;
|
|
1744
1765
|
totalOutput += result.usage.outputTokens;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "11.25.
|
|
3
|
+
"version": "11.25.4",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
|
|
6
6
|
"author": {
|