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.
@@ -1,200 +1,55 @@
1
- const logger = require("../logger");
2
-
3
1
  /**
4
- * Retry configuration for API calls
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: 3,
8
- initialDelay: 1000, // 1 second
9
- maxDelay: 30000, // 30 seconds
10
- backoffMultiplier: 2,
11
- jitterFactor: 0.1, // 10% jitter
12
- retryableStatuses: [429, 500, 502, 503, 504],
13
- retryableErrors: ['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'ENETUNREACH', 'ECONNREFUSED'],
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
- * Add jitter to prevent thundering herd
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
- return addJitter(cappedDelay, config.jitterFactor);
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 (longer than expected response time)
49
+ * Detect if this is a cold start
62
50
  */
63
51
  function detectColdStart(startTime, endTime, threshold = 5000) {
64
- const duration = endTime - startTime;
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 withRetry(() => fn(...args), defaultOptions);
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,
@@ -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
- * Compress while preserving essential information.
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
- compressed.content = compressText(block.content, 500);
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
- return compressText(item, 500);
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
- return {
273
- type: 'text',
274
- text: compressText(item.text, 500)
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
  };