lynkr 8.0.1 → 9.0.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/.lynkr/telemetry.db +0 -0
- package/.lynkr/telemetry.db-shm +0 -0
- package/.lynkr/telemetry.db-wal +0 -0
- package/README.md +195 -321
- package/lynkr-skill.tar.gz +0 -0
- package/package.json +4 -3
- package/src/api/openai-router.js +30 -11
- package/src/api/providers-handler.js +171 -3
- package/src/api/router.js +9 -2
- package/src/clients/circuit-breaker.js +10 -247
- package/src/clients/codex-process.js +342 -0
- package/src/clients/codex-utils.js +143 -0
- package/src/clients/databricks.js +210 -63
- package/src/clients/resilience.js +540 -0
- package/src/clients/retry.js +22 -167
- package/src/config/index.js +57 -0
- package/src/context/compression.js +42 -9
- package/src/context/distill.js +492 -0
- package/src/orchestrator/index.js +46 -6
- package/src/routing/complexity-analyzer.js +258 -5
- package/src/routing/index.js +12 -2
- package/src/routing/latency-tracker.js +148 -0
- package/src/routing/model-tiers.js +2 -0
- package/src/routing/quality-scorer.js +113 -0
- package/src/routing/telemetry.js +464 -0
- package/src/server.js +11 -0
- package/src/tools/code-graph.js +538 -0
- package/src/tools/code-mode.js +304 -0
- package/src/tools/lazy-loader.js +11 -0
- package/src/tools/mcp-remote.js +7 -0
- package/src/tools/smart-selection.js +11 -0
- package/src/utils/payload.js +206 -0
- package/src/utils/perf-timer.js +80 -0
package/src/clients/retry.js
CHANGED
|
@@ -1,200 +1,55 @@
|
|
|
1
|
-
const logger = require("../logger");
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
|
-
* Retry
|
|
2
|
+
* Retry logic for API calls — backed by Cockatiel
|
|
3
|
+
*
|
|
4
|
+
* This module re-exports the Cockatiel-backed retry adapter from resilience.js
|
|
5
|
+
* while preserving all original exports for consumers.
|
|
5
6
|
*/
|
|
7
|
+
const { withCockatielRetry, DEFAULT_RETRY_CONFIG } = require("./resilience");
|
|
8
|
+
|
|
6
9
|
const DEFAULT_CONFIG = {
|
|
7
|
-
maxRetries:
|
|
8
|
-
initialDelay:
|
|
9
|
-
maxDelay:
|
|
10
|
-
backoffMultiplier:
|
|
11
|
-
jitterFactor:
|
|
12
|
-
retryableStatuses:
|
|
13
|
-
retryableErrors:
|
|
10
|
+
maxRetries: DEFAULT_RETRY_CONFIG.maxRetries,
|
|
11
|
+
initialDelay: DEFAULT_RETRY_CONFIG.initialDelay,
|
|
12
|
+
maxDelay: DEFAULT_RETRY_CONFIG.maxDelay,
|
|
13
|
+
backoffMultiplier: DEFAULT_RETRY_CONFIG.backoffMultiplier,
|
|
14
|
+
jitterFactor: DEFAULT_RETRY_CONFIG.jitterFactor,
|
|
15
|
+
retryableStatuses: DEFAULT_RETRY_CONFIG.retryableStatuses,
|
|
16
|
+
retryableErrors: DEFAULT_RETRY_CONFIG.retryableErrors,
|
|
14
17
|
};
|
|
15
18
|
|
|
16
19
|
/**
|
|
17
|
-
*
|
|
18
|
-
*/
|
|
19
|
-
function addJitter(delay, jitterFactor) {
|
|
20
|
-
const jitter = delay * jitterFactor * (Math.random() * 2 - 1);
|
|
21
|
-
return Math.max(0, delay + jitter);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Calculate delay with exponential backoff
|
|
20
|
+
* Calculate delay with exponential backoff (preserved for any direct callers)
|
|
26
21
|
*/
|
|
27
22
|
function calculateDelay(attempt, config) {
|
|
28
23
|
const baseDelay = config.initialDelay * Math.pow(config.backoffMultiplier, attempt);
|
|
29
24
|
const cappedDelay = Math.min(baseDelay, config.maxDelay);
|
|
30
|
-
|
|
25
|
+
const jitter = cappedDelay * config.jitterFactor * (Math.random() * 2 - 1);
|
|
26
|
+
return Math.max(0, cappedDelay + jitter);
|
|
31
27
|
}
|
|
32
28
|
|
|
33
29
|
/**
|
|
34
|
-
* Check if error/response is retryable
|
|
30
|
+
* Check if error/response is retryable (preserved for any direct callers)
|
|
35
31
|
*/
|
|
36
32
|
function isRetryable(error, response, config) {
|
|
37
|
-
// Check response status codes
|
|
38
33
|
if (response && config.retryableStatuses.includes(response.status)) {
|
|
39
34
|
return true;
|
|
40
35
|
}
|
|
41
|
-
|
|
42
|
-
// Check error codes
|
|
43
36
|
if (error && error.code && config.retryableErrors.includes(error.code)) {
|
|
44
37
|
return true;
|
|
45
38
|
}
|
|
46
|
-
|
|
47
|
-
// Check nested cause (Node undici wraps connection errors as TypeError)
|
|
48
39
|
if (error && error.cause?.code && config.retryableErrors.includes(error.cause.code)) {
|
|
49
40
|
return true;
|
|
50
41
|
}
|
|
51
|
-
|
|
52
|
-
// Check for network errors
|
|
53
|
-
if (error && (error.name === 'FetchError' || error.name === 'AbortError')) {
|
|
42
|
+
if (error && (error.name === "FetchError" || error.name === "AbortError")) {
|
|
54
43
|
return true;
|
|
55
44
|
}
|
|
56
|
-
|
|
57
45
|
return false;
|
|
58
46
|
}
|
|
59
47
|
|
|
60
48
|
/**
|
|
61
|
-
* Detect if this is a cold start
|
|
49
|
+
* Detect if this is a cold start
|
|
62
50
|
*/
|
|
63
51
|
function detectColdStart(startTime, endTime, threshold = 5000) {
|
|
64
|
-
|
|
65
|
-
return duration > threshold;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Execute function with retry logic
|
|
70
|
-
*/
|
|
71
|
-
async function withRetry(fn, options = {}) {
|
|
72
|
-
const config = { ...DEFAULT_CONFIG, ...options };
|
|
73
|
-
let lastError;
|
|
74
|
-
let lastResponse;
|
|
75
|
-
|
|
76
|
-
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
|
|
77
|
-
const startTime = Date.now();
|
|
78
|
-
|
|
79
|
-
try {
|
|
80
|
-
const result = await fn(attempt);
|
|
81
|
-
const endTime = Date.now();
|
|
82
|
-
|
|
83
|
-
// Detect cold starts for monitoring
|
|
84
|
-
if (detectColdStart(startTime, endTime)) {
|
|
85
|
-
logger.warn({
|
|
86
|
-
attempt,
|
|
87
|
-
duration: endTime - startTime,
|
|
88
|
-
}, 'Potential cold start detected');
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Check if response indicates we should retry
|
|
92
|
-
if (result && isRetryable(null, result, config) && attempt < config.maxRetries) {
|
|
93
|
-
lastResponse = result;
|
|
94
|
-
|
|
95
|
-
// Special handling for 429 (rate limiting)
|
|
96
|
-
if (result.status === 429) {
|
|
97
|
-
// Check for Retry-After header
|
|
98
|
-
const retryAfter = result.headers?.get?.('retry-after');
|
|
99
|
-
let delay;
|
|
100
|
-
|
|
101
|
-
if (retryAfter) {
|
|
102
|
-
// Retry-After can be in seconds or a date
|
|
103
|
-
const retryAfterNum = parseInt(retryAfter, 10);
|
|
104
|
-
if (!isNaN(retryAfterNum)) {
|
|
105
|
-
delay = retryAfterNum * 1000; // Convert to ms
|
|
106
|
-
} else {
|
|
107
|
-
const retryAfterDate = new Date(retryAfter);
|
|
108
|
-
delay = retryAfterDate.getTime() - Date.now();
|
|
109
|
-
}
|
|
110
|
-
} else {
|
|
111
|
-
// Use exponential backoff with longer delays for rate limiting
|
|
112
|
-
delay = calculateDelay(attempt, {
|
|
113
|
-
...config,
|
|
114
|
-
initialDelay: 2000, // Start at 2s for rate limits
|
|
115
|
-
maxDelay: 60000, // Up to 1 minute
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
logger.warn({
|
|
120
|
-
attempt,
|
|
121
|
-
delay,
|
|
122
|
-
retryAfter: retryAfter || 'not specified',
|
|
123
|
-
}, 'Rate limited (429), retrying after delay');
|
|
124
|
-
|
|
125
|
-
await sleep(delay);
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Regular retry with exponential backoff
|
|
130
|
-
const delay = calculateDelay(attempt, config);
|
|
131
|
-
logger.warn({
|
|
132
|
-
attempt,
|
|
133
|
-
status: result.status,
|
|
134
|
-
delay,
|
|
135
|
-
}, 'Request failed, retrying with backoff');
|
|
136
|
-
|
|
137
|
-
await sleep(delay);
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Success or non-retryable error
|
|
142
|
-
return result;
|
|
143
|
-
|
|
144
|
-
} catch (error) {
|
|
145
|
-
lastError = error;
|
|
146
|
-
const endTime = Date.now();
|
|
147
|
-
|
|
148
|
-
// Check if cold start
|
|
149
|
-
if (detectColdStart(startTime, endTime)) {
|
|
150
|
-
logger.warn({
|
|
151
|
-
attempt,
|
|
152
|
-
duration: endTime - startTime,
|
|
153
|
-
error: error.message,
|
|
154
|
-
}, 'Potential cold start with error detected');
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Check if we should retry
|
|
158
|
-
if (isRetryable(error, null, config) && attempt < config.maxRetries) {
|
|
159
|
-
const delay = calculateDelay(attempt, config);
|
|
160
|
-
logger.warn({
|
|
161
|
-
attempt,
|
|
162
|
-
error: error.message,
|
|
163
|
-
code: error.code,
|
|
164
|
-
delay,
|
|
165
|
-
}, 'Request error, retrying with backoff');
|
|
166
|
-
|
|
167
|
-
await sleep(delay);
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Not retryable or out of retries
|
|
172
|
-
throw error;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Max retries exceeded
|
|
177
|
-
if (lastError) {
|
|
178
|
-
lastError.message = `Max retries (${config.maxRetries}) exceeded: ${lastError.message}`;
|
|
179
|
-
throw lastError;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (lastResponse) {
|
|
183
|
-
logger.error({
|
|
184
|
-
status: lastResponse.status,
|
|
185
|
-
maxRetries: config.maxRetries,
|
|
186
|
-
}, 'Max retries exceeded');
|
|
187
|
-
return lastResponse;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
throw new Error('Retry logic failed unexpectedly');
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Sleep helper
|
|
195
|
-
*/
|
|
196
|
-
function sleep(ms) {
|
|
197
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
52
|
+
return (endTime - startTime) > threshold;
|
|
198
53
|
}
|
|
199
54
|
|
|
200
55
|
/**
|
|
@@ -202,12 +57,12 @@ function sleep(ms) {
|
|
|
202
57
|
*/
|
|
203
58
|
function createRetryWrapper(fn, defaultOptions = {}) {
|
|
204
59
|
return async function (...args) {
|
|
205
|
-
return
|
|
60
|
+
return withCockatielRetry(() => fn(...args), defaultOptions);
|
|
206
61
|
};
|
|
207
62
|
}
|
|
208
63
|
|
|
209
64
|
module.exports = {
|
|
210
|
-
withRetry,
|
|
65
|
+
withRetry: withCockatielRetry,
|
|
211
66
|
createRetryWrapper,
|
|
212
67
|
calculateDelay,
|
|
213
68
|
isRetryable,
|
package/src/config/index.js
CHANGED
|
@@ -618,6 +618,12 @@ var config = {
|
|
|
618
618
|
endpoint: moonshotEndpoint,
|
|
619
619
|
model: moonshotModel,
|
|
620
620
|
},
|
|
621
|
+
codex: {
|
|
622
|
+
enabled: process.env.CODEX_ENABLED !== "false",
|
|
623
|
+
binaryPath: process.env.CODEX_BINARY_PATH?.trim() || null,
|
|
624
|
+
model: process.env.CODEX_MODEL?.trim() || "gpt-5.3-codex",
|
|
625
|
+
timeout: Number.parseInt(process.env.CODEX_TIMEOUT || "120000", 10) || 120000,
|
|
626
|
+
},
|
|
621
627
|
hotReload: {
|
|
622
628
|
enabled: hotReloadEnabled,
|
|
623
629
|
debounceMs: Number.isNaN(hotReloadDebounceMs) ? 1000 : hotReloadDebounceMs,
|
|
@@ -733,6 +739,10 @@ var config = {
|
|
|
733
739
|
manifestPath: sandboxManifestPath,
|
|
734
740
|
manifestDirs: sandboxManifestDirs,
|
|
735
741
|
},
|
|
742
|
+
codeMode: {
|
|
743
|
+
enabled: process.env.CODE_MODE_ENABLED === 'true',
|
|
744
|
+
toolListCacheTtl: parseInt(process.env.CODE_MODE_CACHE_TTL, 10) || 60_000,
|
|
745
|
+
},
|
|
736
746
|
},
|
|
737
747
|
promptCache: {
|
|
738
748
|
enabled: promptCacheEnabled,
|
|
@@ -918,6 +928,25 @@ var config = {
|
|
|
918
928
|
COMPLEX: process.env.TIER_COMPLEX?.trim() || null,
|
|
919
929
|
REASONING: process.env.TIER_REASONING?.trim() || null,
|
|
920
930
|
},
|
|
931
|
+
|
|
932
|
+
// Graphify knowledge graph integration (structural analysis)
|
|
933
|
+
codeGraph: {
|
|
934
|
+
enabled: process.env.CODE_GRAPH_ENABLED === 'true',
|
|
935
|
+
command: process.env.CODE_GRAPH_COMMAND || 'graphify',
|
|
936
|
+
workspace: process.env.CODE_GRAPH_WORKSPACE || process.cwd(),
|
|
937
|
+
timeout: parseInt(process.env.CODE_GRAPH_TIMEOUT, 10) || 10000,
|
|
938
|
+
},
|
|
939
|
+
|
|
940
|
+
// Large payload optimization (skip cloning media blocks that get discarded)
|
|
941
|
+
largePayload: {
|
|
942
|
+
enabled: process.env.LARGE_PAYLOAD_OPTIMIZATION !== 'false',
|
|
943
|
+
threshold: parseInt(process.env.LARGE_PAYLOAD_THRESHOLD, 10) || 1_048_576,
|
|
944
|
+
},
|
|
945
|
+
|
|
946
|
+
// OpenClaw integration
|
|
947
|
+
openclaw: {
|
|
948
|
+
enabled: process.env.OPENCLAW_MODE === "true",
|
|
949
|
+
},
|
|
921
950
|
};
|
|
922
951
|
|
|
923
952
|
/**
|
|
@@ -964,9 +993,37 @@ function reloadConfig() {
|
|
|
964
993
|
config.toon.failOpen = process.env.TOON_FAIL_OPEN !== "false";
|
|
965
994
|
config.toon.logStats = process.env.TOON_LOG_STATS !== "false";
|
|
966
995
|
|
|
996
|
+
// Tier routing (critical for fixing model name issues without restart)
|
|
997
|
+
config.modelTiers.SIMPLE = process.env.TIER_SIMPLE?.trim() || null;
|
|
998
|
+
config.modelTiers.MEDIUM = process.env.TIER_MEDIUM?.trim() || null;
|
|
999
|
+
config.modelTiers.COMPLEX = process.env.TIER_COMPLEX?.trim() || null;
|
|
1000
|
+
config.modelTiers.REASONING = process.env.TIER_REASONING?.trim() || null;
|
|
1001
|
+
config.modelTiers.enabled = !!(config.modelTiers.SIMPLE && config.modelTiers.MEDIUM && config.modelTiers.COMPLEX && config.modelTiers.REASONING);
|
|
1002
|
+
|
|
1003
|
+
// Ollama model
|
|
1004
|
+
config.ollama.endpoint = process.env.OLLAMA_ENDPOINT ?? config.ollama.endpoint;
|
|
1005
|
+
|
|
1006
|
+
// OpenClaw
|
|
1007
|
+
config.openclaw.enabled = process.env.OPENCLAW_MODE === "true";
|
|
1008
|
+
|
|
1009
|
+
// Graphify
|
|
1010
|
+
config.codeGraph.enabled = process.env.CODE_GRAPH_ENABLED === 'true';
|
|
1011
|
+
|
|
1012
|
+
// Code Mode
|
|
1013
|
+
config.mcp.codeMode.enabled = process.env.CODE_MODE_ENABLED === 'true';
|
|
1014
|
+
|
|
967
1015
|
// Log level
|
|
968
1016
|
config.logger.level = process.env.LOG_LEVEL ?? "info";
|
|
969
1017
|
|
|
1018
|
+
// Reset circuit breakers so stale OPEN states don't persist
|
|
1019
|
+
try {
|
|
1020
|
+
const { getCircuitBreakerRegistry } = require('../clients/circuit-breaker');
|
|
1021
|
+
getCircuitBreakerRegistry().resetAll();
|
|
1022
|
+
console.log("[CONFIG] Circuit breakers reset");
|
|
1023
|
+
} catch (e) {
|
|
1024
|
+
// Ignore if not yet initialized
|
|
1025
|
+
}
|
|
1026
|
+
|
|
970
1027
|
console.log("[CONFIG] Configuration reloaded from environment");
|
|
971
1028
|
return config;
|
|
972
1029
|
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
const logger = require('../logger');
|
|
13
13
|
const config = require('../config');
|
|
14
|
+
const distill = require('./distill');
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Compress conversation history to fit within token budget
|
|
@@ -62,6 +63,18 @@ function compressHistory(messages, options = {}) {
|
|
|
62
63
|
compressed = oldMessages.map(msg => compressMessage(msg));
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
// Apply Distill dedup across all old messages to collapse repetitive tool results
|
|
67
|
+
if (compressed.length > 0) {
|
|
68
|
+
const dedupResult = distill.deduplicateHistory(compressed);
|
|
69
|
+
if (dedupResult.stats.deduplicated > 0) {
|
|
70
|
+
compressed = dedupResult.messages;
|
|
71
|
+
logger.debug({
|
|
72
|
+
checked: dedupResult.stats.checked,
|
|
73
|
+
deduplicated: dedupResult.stats.deduplicated,
|
|
74
|
+
}, '[Distill] History dedup applied to old messages');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
65
78
|
// Add recent messages (may compress tool results but keep content)
|
|
66
79
|
const recentCompressed = recentMessages.map(msg => compressToolResults(msg));
|
|
67
80
|
|
|
@@ -248,12 +261,15 @@ function compressContentBlock(block) {
|
|
|
248
261
|
* Compress tool result block
|
|
249
262
|
*
|
|
250
263
|
* Tool results can be very large (file contents, bash output).
|
|
251
|
-
*
|
|
264
|
+
* Uses Distill algorithms (normalization, delta, dedup) before
|
|
265
|
+
* falling back to head/tail truncation.
|
|
252
266
|
*
|
|
253
267
|
* @param {Object} block - tool_result block
|
|
268
|
+
* @param {Object} options - Options for compression
|
|
269
|
+
* @param {string} options.previousResult - Previous tool result for delta rendering
|
|
254
270
|
* @returns {Object} Compressed tool_result
|
|
255
271
|
*/
|
|
256
|
-
function compressToolResultBlock(block) {
|
|
272
|
+
function compressToolResultBlock(block, options = {}) {
|
|
257
273
|
if (!block || block.type !== 'tool_result') return block;
|
|
258
274
|
|
|
259
275
|
const compressed = {
|
|
@@ -261,18 +277,32 @@ function compressToolResultBlock(block) {
|
|
|
261
277
|
tool_use_id: block.tool_use_id,
|
|
262
278
|
};
|
|
263
279
|
|
|
264
|
-
// Compress content
|
|
280
|
+
// Compress content using Distill when content is large enough to benefit
|
|
265
281
|
if (typeof block.content === 'string') {
|
|
266
|
-
|
|
282
|
+
if (block.content.length > 500) {
|
|
283
|
+
const result = distill.compressToolResult(block.content, {
|
|
284
|
+
previousResult: options.previousResult,
|
|
285
|
+
maxLength: 500,
|
|
286
|
+
});
|
|
287
|
+
compressed.content = result.text;
|
|
288
|
+
} else {
|
|
289
|
+
compressed.content = block.content;
|
|
290
|
+
}
|
|
267
291
|
} else if (Array.isArray(block.content)) {
|
|
268
292
|
compressed.content = block.content.map(item => {
|
|
269
293
|
if (typeof item === 'string') {
|
|
270
|
-
|
|
294
|
+
if (item.length > 500) {
|
|
295
|
+
return distill.compressToolResult(item, { maxLength: 500 }).text;
|
|
296
|
+
}
|
|
297
|
+
return item;
|
|
271
298
|
} else if (item.type === 'text') {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
299
|
+
if (item.text && item.text.length > 500) {
|
|
300
|
+
return {
|
|
301
|
+
type: 'text',
|
|
302
|
+
text: distill.compressToolResult(item.text, { maxLength: 500 }).text,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
return item;
|
|
276
306
|
}
|
|
277
307
|
return item;
|
|
278
308
|
});
|
|
@@ -453,7 +483,10 @@ module.exports = {
|
|
|
453
483
|
compressHistory,
|
|
454
484
|
compressMessage,
|
|
455
485
|
compressToolResults,
|
|
486
|
+
compressToolResultBlock,
|
|
456
487
|
calculateCompressionStats,
|
|
457
488
|
needsCompression,
|
|
458
489
|
summarizeOldHistory,
|
|
490
|
+
// Distill re-exports for direct access
|
|
491
|
+
distill,
|
|
459
492
|
};
|