lynkr 7.2.4 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +2 -2
  2. package/config/model-tiers.json +89 -0
  3. package/docs/docs.html +1 -0
  4. package/docs/index.md +7 -0
  5. package/docs/toon-integration-spec.md +130 -0
  6. package/documentation/README.md +3 -2
  7. package/documentation/claude-code-cli.md +23 -16
  8. package/documentation/cursor-integration.md +17 -14
  9. package/documentation/docker.md +11 -4
  10. package/documentation/embeddings.md +7 -5
  11. package/documentation/faq.md +66 -12
  12. package/documentation/features.md +22 -15
  13. package/documentation/installation.md +66 -14
  14. package/documentation/production.md +43 -8
  15. package/documentation/providers.md +145 -42
  16. package/documentation/routing.md +476 -0
  17. package/documentation/token-optimization.md +7 -5
  18. package/documentation/troubleshooting.md +81 -5
  19. package/install.sh +6 -1
  20. package/package.json +5 -3
  21. package/scripts/setup.js +0 -1
  22. package/src/agents/executor.js +14 -6
  23. package/src/api/middleware/session.js +15 -2
  24. package/src/api/openai-router.js +130 -37
  25. package/src/api/providers-handler.js +15 -1
  26. package/src/api/router.js +107 -2
  27. package/src/budget/index.js +4 -3
  28. package/src/clients/databricks.js +431 -234
  29. package/src/clients/gpt-utils.js +181 -0
  30. package/src/clients/ollama-utils.js +66 -140
  31. package/src/clients/routing.js +0 -1
  32. package/src/clients/standard-tools.js +82 -5
  33. package/src/config/index.js +119 -35
  34. package/src/context/toon.js +173 -0
  35. package/src/headroom/launcher.js +8 -3
  36. package/src/logger/index.js +23 -0
  37. package/src/orchestrator/index.js +765 -212
  38. package/src/routing/agentic-detector.js +320 -0
  39. package/src/routing/complexity-analyzer.js +202 -2
  40. package/src/routing/cost-optimizer.js +305 -0
  41. package/src/routing/index.js +168 -159
  42. package/src/routing/model-registry.js +437 -0
  43. package/src/routing/model-tiers.js +365 -0
  44. package/src/server.js +2 -2
  45. package/src/sessions/cleanup.js +3 -3
  46. package/src/sessions/record.js +10 -1
  47. package/src/sessions/store.js +7 -2
  48. package/src/tools/agent-task.js +48 -1
  49. package/src/tools/index.js +15 -2
  50. package/src/tools/workspace.js +35 -4
  51. package/src/workspace/index.js +30 -0
  52. package/te +11622 -0
  53. package/test/README.md +1 -1
  54. package/test/azure-openai-config.test.js +17 -8
  55. package/test/azure-openai-integration.test.js +7 -1
  56. package/test/azure-openai-routing.test.js +41 -43
  57. package/test/bedrock-integration.test.js +18 -32
  58. package/test/hybrid-routing-integration.test.js +35 -20
  59. package/test/hybrid-routing-performance.test.js +74 -64
  60. package/test/llamacpp-integration.test.js +28 -9
  61. package/test/lmstudio-integration.test.js +20 -8
  62. package/test/openai-integration.test.js +17 -20
  63. package/test/performance-tests.js +1 -1
  64. package/test/routing.test.js +65 -59
  65. package/test/toon-compression.test.js +131 -0
  66. package/CLAWROUTER_ROUTING_PLAN.md +0 -910
  67. package/ROUTER_COMPARISON.md +0 -173
  68. package/TIER_ROUTING_PLAN.md +0 -771
@@ -62,7 +62,7 @@ function resolveConfigPath(targetPath) {
62
62
  return path.resolve(normalised);
63
63
  }
64
64
 
65
- const SUPPORTED_MODEL_PROVIDERS = new Set(["databricks", "azure-anthropic", "ollama", "openrouter", "azure-openai", "openai", "llamacpp", "lmstudio", "bedrock", "zai", "vertex"]);
65
+ const SUPPORTED_MODEL_PROVIDERS = new Set(["databricks", "azure-anthropic", "ollama", "openrouter", "azure-openai", "openai", "llamacpp", "lmstudio", "bedrock", "zai", "vertex", "moonshot"]);
66
66
  const rawModelProvider = (process.env.MODEL_PROVIDER ?? "databricks").toLowerCase();
67
67
 
68
68
  // Validate MODEL_PROVIDER early with a clear error message
@@ -132,16 +132,24 @@ const zaiApiKey = process.env.ZAI_API_KEY?.trim() || null;
132
132
  const zaiEndpoint = process.env.ZAI_ENDPOINT?.trim() || "https://api.z.ai/api/anthropic/v1/messages";
133
133
  const zaiModel = process.env.ZAI_MODEL?.trim() || "GLM-4.7";
134
134
 
135
+ // Moonshot AI (Kimi) configuration - OpenAI-compatible API
136
+ const moonshotApiKey = process.env.MOONSHOT_API_KEY?.trim() || null;
137
+ const moonshotEndpoint = process.env.MOONSHOT_ENDPOINT?.trim() || "https://api.moonshot.ai/v1/chat/completions";
138
+ const moonshotModel = process.env.MOONSHOT_MODEL?.trim() || "kimi-k2-turbo-preview";
139
+
135
140
  // Vertex AI (Google Gemini) configuration
136
141
  const vertexApiKey = process.env.VERTEX_API_KEY?.trim() || process.env.GOOGLE_API_KEY?.trim() || null;
137
142
  const vertexModel = process.env.VERTEX_MODEL?.trim() || "gemini-2.0-flash";
138
143
 
144
+ // Suggestion mode model override
145
+ // Values: "default" (use MODEL_DEFAULT), "none" (skip LLM call), or a model name
146
+ const suggestionModeModel = (process.env.SUGGESTION_MODE_MODEL ?? "default").trim();
147
+
139
148
  // Hot reload configuration
140
149
  const hotReloadEnabled = process.env.HOT_RELOAD_ENABLED !== "false"; // default true
141
150
  const hotReloadDebounceMs = Number.parseInt(process.env.HOT_RELOAD_DEBOUNCE_MS ?? "1000", 10);
142
151
 
143
- // Hybrid routing configuration
144
- const preferOllama = process.env.PREFER_OLLAMA === "true";
152
+ // Routing configuration
145
153
  const fallbackEnabled = process.env.FALLBACK_ENABLED !== "false"; // default true
146
154
  const ollamaMaxToolsForRouting = Number.parseInt(
147
155
  process.env.OLLAMA_MAX_TOOLS_FOR_ROUTING ?? "3",
@@ -200,6 +208,12 @@ const tokenBudgetWarning = Number.parseInt(process.env.TOKEN_BUDGET_WARNING ?? "
200
208
  const tokenBudgetMax = Number.parseInt(process.env.TOKEN_BUDGET_MAX ?? "180000", 10);
201
209
  const tokenBudgetEnforcement = process.env.TOKEN_BUDGET_ENFORCEMENT !== "false"; // default true
202
210
 
211
+ // TOON payload compression (opt-in)
212
+ const toonEnabled = process.env.TOON_ENABLED === "true"; // default false
213
+ const toonMinBytes = Number.parseInt(process.env.TOON_MIN_BYTES ?? "4096", 10);
214
+ const toonFailOpen = process.env.TOON_FAIL_OPEN !== "false"; // default true
215
+ const toonLogStats = process.env.TOON_LOG_STATS !== "false"; // default true
216
+
203
217
  // Smart tool selection configuration (always enabled)
204
218
  const smartToolSelectionMode = (process.env.SMART_TOOL_SELECTION_MODE ?? "heuristic").toLowerCase();
205
219
  const smartToolSelectionTokenBudget = Number.parseInt(
@@ -301,37 +315,39 @@ if (modelProvider === "bedrock" && !bedrockApiKey) {
301
315
  );
302
316
  }
303
317
 
304
- // Validate hybrid routing configuration
305
- if (preferOllama) {
306
- if (!ollamaEndpoint) {
307
- throw new Error("PREFER_OLLAMA is set but OLLAMA_ENDPOINT is not configured");
308
- }
309
- if (fallbackEnabled && !SUPPORTED_MODEL_PROVIDERS.has(fallbackProvider)) {
310
- throw new Error(
311
- `FALLBACK_PROVIDER must be one of: ${Array.from(SUPPORTED_MODEL_PROVIDERS).join(", ")}`
312
- );
313
- }
318
+ // Deprecation warning for PREFER_OLLAMA
319
+ if (process.env.PREFER_OLLAMA) {
320
+ console.warn('[DEPRECATION] PREFER_OLLAMA is removed. Use TIER_* env vars for routing. See documentation/routing.md');
321
+ }
314
322
 
315
- // Prevent local providers from being used as fallback (they can fail just like Ollama)
323
+ // Warn about misconfigured fallback provider (only when tier routing is active,
324
+ // since that's the only path that triggers provider fallback)
325
+ const tiersConfigured = !!(
326
+ process.env.TIER_SIMPLE?.trim() &&
327
+ process.env.TIER_MEDIUM?.trim() &&
328
+ process.env.TIER_COMPLEX?.trim() &&
329
+ process.env.TIER_REASONING?.trim()
330
+ );
331
+ if (fallbackEnabled && tiersConfigured) {
316
332
  const localProviders = ["ollama", "llamacpp", "lmstudio"];
317
- if (fallbackEnabled && localProviders.includes(fallbackProvider)) {
333
+ if (localProviders.includes(fallbackProvider)) {
318
334
  throw new Error(`FALLBACK_PROVIDER cannot be '${fallbackProvider}' (local providers should not be fallbacks). Use cloud providers: databricks, azure-anthropic, azure-openai, openrouter, openai, bedrock`);
319
335
  }
320
-
321
- // Ensure fallback provider is properly configured (only if fallback is enabled)
322
- if (fallbackEnabled) {
323
- if (fallbackProvider === "databricks" && (!rawBaseUrl || !apiKey)) {
324
- throw new Error("FALLBACK_PROVIDER is set to 'databricks' but DATABRICKS_API_BASE and DATABRICKS_API_KEY are not configured. Please set these environment variables or choose a different fallback provider.");
325
- }
326
- if (fallbackProvider === "azure-anthropic" && (!azureAnthropicEndpoint || !azureAnthropicApiKey)) {
327
- throw new Error("FALLBACK_PROVIDER is set to 'azure-anthropic' but AZURE_ANTHROPIC_ENDPOINT and AZURE_ANTHROPIC_API_KEY are not configured. Please set these environment variables or choose a different fallback provider.");
328
- }
329
- if (fallbackProvider === "azure-openai" && (!azureOpenAIEndpoint || !azureOpenAIApiKey)) {
330
- throw new Error("FALLBACK_PROVIDER is set to 'azure-openai' but AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_API_KEY are not configured. Please set these environment variables or choose a different fallback provider.");
331
- }
332
- if (fallbackProvider === "bedrock" && !bedrockApiKey) {
333
- throw new Error("FALLBACK_PROVIDER is set to 'bedrock' but AWS_BEDROCK_API_KEY is not configured. Please set this environment variable or choose a different fallback provider.");
334
- }
336
+ let fallbackMisconfigured = false;
337
+ if (fallbackProvider === "databricks" && (!rawBaseUrl || !apiKey)) {
338
+ fallbackMisconfigured = true;
339
+ }
340
+ if (fallbackProvider === "azure-anthropic" && (!azureAnthropicEndpoint || !azureAnthropicApiKey)) {
341
+ fallbackMisconfigured = true;
342
+ }
343
+ if (fallbackProvider === "azure-openai" && (!azureOpenAIEndpoint || !azureOpenAIApiKey)) {
344
+ fallbackMisconfigured = true;
345
+ }
346
+ if (fallbackProvider === "bedrock" && !bedrockApiKey) {
347
+ fallbackMisconfigured = true;
348
+ }
349
+ if (fallbackMisconfigured) {
350
+ console.warn(`[WARN] FALLBACK_PROVIDER='${fallbackProvider}' is enabled but missing credentials. Fallback will not work until configured.`);
335
351
  }
336
352
  }
337
353
 
@@ -589,6 +605,11 @@ var config = {
589
605
  apiKey: vertexApiKey,
590
606
  model: vertexModel,
591
607
  },
608
+ moonshot: {
609
+ apiKey: moonshotApiKey,
610
+ endpoint: moonshotEndpoint,
611
+ model: moonshotModel,
612
+ },
592
613
  hotReload: {
593
614
  enabled: hotReloadEnabled,
594
615
  debounceMs: Number.isNaN(hotReloadDebounceMs) ? 1000 : hotReloadDebounceMs,
@@ -596,8 +617,7 @@ var config = {
596
617
  modelProvider: {
597
618
  type: modelProvider,
598
619
  defaultModel,
599
- // Hybrid routing settings
600
- preferOllama,
620
+ suggestionModeModel,
601
621
  fallbackEnabled,
602
622
  ollamaMaxToolsForRouting,
603
623
  openRouterMaxToolsForRouting,
@@ -615,6 +635,13 @@ var config = {
615
635
  },
616
636
  logger: {
617
637
  level: process.env.LOG_LEVEL ?? "info",
638
+ file: {
639
+ enabled: process.env.LOG_FILE_ENABLED === "true",
640
+ path: process.env.LOG_FILE_PATH ?? path.join(process.cwd(), "logs", "lynkr.log"),
641
+ level: process.env.LOG_FILE_LEVEL ?? "debug", // File captures everything
642
+ frequency: process.env.LOG_FILE_FREQUENCY ?? "daily", // daily | hourly | <milliseconds>
643
+ maxFiles: parseInt(process.env.LOG_FILE_MAX_FILES ?? "14", 10),
644
+ },
618
645
  },
619
646
  sessionStore: {
620
647
  dbPath: sessionDbPath,
@@ -699,8 +726,8 @@ var config = {
699
726
  semanticCache: {
700
727
  enabled: process.env.SEMANTIC_CACHE_ENABLED !== 'false', // Disable via env if needed
701
728
  similarityThreshold: parseFloat(process.env.SEMANTIC_CACHE_THRESHOLD || '0.95'), // Higher threshold
702
- maxEntries: 500,
703
- ttlMs: 3600000, // 1 hour
729
+ maxEntries: Number.parseInt(process.env.SEMANTIC_CACHE_MAX_ENTRIES ?? "50", 10), // Reduced from 500 to prevent memory bloat
730
+ ttlMs: Number.parseInt(process.env.SEMANTIC_CACHE_TTL_MS ?? "300000", 10), // 5 minutes (was 1 hour)
704
731
  },
705
732
  agents: {
706
733
  enabled: agentsEnabled,
@@ -760,6 +787,12 @@ var config = {
760
787
  max: tokenBudgetMax,
761
788
  enforcement: tokenBudgetEnforcement,
762
789
  },
790
+ toon: {
791
+ enabled: toonEnabled,
792
+ minBytes: Number.isNaN(toonMinBytes) ? 4096 : toonMinBytes,
793
+ failOpen: toonFailOpen,
794
+ logStats: toonLogStats,
795
+ },
763
796
  smartToolSelection: {
764
797
  enabled: true, // HARDCODED - always enabled
765
798
  mode: smartToolSelectionMode,
@@ -852,6 +885,23 @@ var config = {
852
885
  taskTimeoutMs: Number.isNaN(workerTaskTimeoutMs) ? 5000 : workerTaskTimeoutMs,
853
886
  offloadThresholdBytes: Number.isNaN(workerOffloadThresholdBytes) ? 10000 : workerOffloadThresholdBytes,
854
887
  },
888
+
889
+ // Intelligent Routing
890
+ routing: {
891
+ weightedScoring: true,
892
+ costOptimization: true,
893
+ agenticDetection: true,
894
+ },
895
+
896
+ // Model Tier Configuration (REQUIRED)
897
+ // Format: TIER_<LEVEL>=provider:model (e.g., TIER_SIMPLE=ollama:llama3.2)
898
+ modelTiers: {
899
+ enabled: true,
900
+ SIMPLE: process.env.TIER_SIMPLE?.trim() || null,
901
+ MEDIUM: process.env.TIER_MEDIUM?.trim() || null,
902
+ COMPLEX: process.env.TIER_COMPLEX?.trim() || null,
903
+ REASONING: process.env.TIER_REASONING?.trim() || null,
904
+ },
855
905
  };
856
906
 
857
907
  /**
@@ -876,15 +926,23 @@ function reloadConfig() {
876
926
  config.zai.model = process.env.ZAI_MODEL?.trim() || "GLM-4.7";
877
927
  config.vertex.apiKey = process.env.VERTEX_API_KEY?.trim() || process.env.GOOGLE_API_KEY?.trim() || null;
878
928
  config.vertex.model = process.env.VERTEX_MODEL?.trim() || "gemini-2.0-flash";
929
+ config.moonshot.apiKey = process.env.MOONSHOT_API_KEY?.trim() || null;
930
+ config.moonshot.model = process.env.MOONSHOT_MODEL?.trim() || "kimi-k2-turbo-preview";
879
931
 
880
932
  // Model provider settings
881
933
  const newProvider = (process.env.MODEL_PROVIDER ?? "databricks").toLowerCase();
882
934
  if (SUPPORTED_MODEL_PROVIDERS.has(newProvider)) {
883
935
  config.modelProvider.type = newProvider;
884
936
  }
885
- config.modelProvider.preferOllama = process.env.PREFER_OLLAMA === "true";
886
937
  config.modelProvider.fallbackEnabled = process.env.FALLBACK_ENABLED !== "false";
887
938
  config.modelProvider.fallbackProvider = (process.env.FALLBACK_PROVIDER ?? "databricks").toLowerCase();
939
+ config.modelProvider.suggestionModeModel = (process.env.SUGGESTION_MODE_MODEL ?? "default").trim();
940
+
941
+ config.toon.enabled = process.env.TOON_ENABLED === "true";
942
+ const newToonMinBytes = Number.parseInt(process.env.TOON_MIN_BYTES ?? "4096", 10);
943
+ config.toon.minBytes = Number.isNaN(newToonMinBytes) ? 4096 : newToonMinBytes;
944
+ config.toon.failOpen = process.env.TOON_FAIL_OPEN !== "false";
945
+ config.toon.logStats = process.env.TOON_LOG_STATS !== "false";
888
946
 
889
947
  // Log level
890
948
  config.logger.level = process.env.LOG_LEVEL ?? "info";
@@ -896,4 +954,30 @@ function reloadConfig() {
896
954
  // Make config mutable for hot reload
897
955
  config.reloadConfig = reloadConfig;
898
956
 
957
+ /**
958
+ * Check if any TIER_* value references Ollama (starts with "ollama:")
959
+ * Used by server.js to decide whether to wait for Ollama at startup.
960
+ */
961
+ config.tiersReferenceOllama = function tiersReferenceOllama() {
962
+ const tiers = config.modelTiers;
963
+ if (!tiers?.enabled) return false;
964
+ return [tiers.SIMPLE, tiers.MEDIUM, tiers.COMPLEX, tiers.REASONING]
965
+ .some(v => typeof v === 'string' && v.startsWith('ollama:'));
966
+ };
967
+
968
+ // Validate TIER_* configuration (warn if missing, don't crash)
969
+ const missingTiers = [];
970
+ if (!config.modelTiers.SIMPLE) missingTiers.push('TIER_SIMPLE');
971
+ if (!config.modelTiers.MEDIUM) missingTiers.push('TIER_MEDIUM');
972
+ if (!config.modelTiers.COMPLEX) missingTiers.push('TIER_COMPLEX');
973
+ if (!config.modelTiers.REASONING) missingTiers.push('TIER_REASONING');
974
+
975
+ if (missingTiers.length > 0) {
976
+ config.modelTiers.enabled = false;
977
+ console.warn(
978
+ `[WARN] Missing tier configuration: ${missingTiers.join(', ')} — tiered routing disabled.\n` +
979
+ ` Set TIER_<LEVEL>=provider:model to enable (e.g., TIER_SIMPLE=ollama:llama3.2)`
980
+ );
981
+ }
982
+
899
983
  module.exports = config;
@@ -0,0 +1,173 @@
1
+ const logger = require("../logger");
2
+
3
+ let cachedEncode;
4
+ let cachedLoadError;
5
+ let warnedMissingDependency = false;
6
+
7
+ function normaliseSettings(settings = {}) {
8
+ const minBytesRaw =
9
+ typeof settings.minBytes === "number" ? settings.minBytes : Number.parseInt(settings.minBytes ?? "4096", 10);
10
+ return {
11
+ enabled: settings.enabled === true,
12
+ minBytes: Number.isFinite(minBytesRaw) && minBytesRaw > 0 ? minBytesRaw : 4096,
13
+ failOpen: settings.failOpen !== false,
14
+ logStats: settings.logStats !== false,
15
+ };
16
+ }
17
+
18
+ function resolveEncodeFn(overrideEncode) {
19
+ if (typeof overrideEncode === "function") return overrideEncode;
20
+ if (cachedEncode !== undefined) return cachedEncode;
21
+ try {
22
+ const toon = require("@toon-format/toon");
23
+ cachedEncode = typeof toon?.encode === "function" ? toon.encode : null;
24
+ cachedLoadError = cachedEncode ? null : new Error("Missing encode() export from @toon-format/toon");
25
+ } catch (err) {
26
+ cachedEncode = null;
27
+ cachedLoadError = err;
28
+ }
29
+ return cachedEncode;
30
+ }
31
+
32
+ function looksLikeJsonObjectOrArray(text) {
33
+ if (typeof text !== "string") return false;
34
+ const trimmed = text.trim();
35
+ if (trimmed.length < 2) return false;
36
+ return (
37
+ (trimmed.startsWith("{") && trimmed.endsWith("}")) ||
38
+ (trimmed.startsWith("[") && trimmed.endsWith("]"))
39
+ );
40
+ }
41
+
42
+ function safeJsonParse(text) {
43
+ try {
44
+ return JSON.parse(text);
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ function toToonString(encodeFn, value) {
51
+ const encoded = encodeFn(value);
52
+ if (typeof encoded === "string") return encoded;
53
+ if (encoded && typeof encoded[Symbol.iterator] === "function") {
54
+ return Array.from(encoded).join("\n");
55
+ }
56
+ return "";
57
+ }
58
+
59
+ function compressStringContent(content, cfg, encodeFn, stats) {
60
+ if (typeof content !== "string") return content;
61
+
62
+ const originalBytes = Buffer.byteLength(content, "utf8");
63
+ if (originalBytes < cfg.minBytes) {
64
+ stats.skippedBySize += 1;
65
+ return content;
66
+ }
67
+
68
+ stats.candidateCount += 1;
69
+ if (!looksLikeJsonObjectOrArray(content)) {
70
+ stats.skippedByShape += 1;
71
+ return content;
72
+ }
73
+
74
+ const parsed = safeJsonParse(content);
75
+ if (!parsed || typeof parsed !== "object") {
76
+ stats.skippedByParse += 1;
77
+ return content;
78
+ }
79
+
80
+ const toonText = toToonString(encodeFn, parsed);
81
+ if (typeof toonText !== "string" || toonText.trim().length === 0) {
82
+ return content;
83
+ }
84
+
85
+ const compressedBytes = Buffer.byteLength(toonText, "utf8");
86
+ stats.convertedCount += 1;
87
+ stats.originalBytes += originalBytes;
88
+ stats.compressedBytes += compressedBytes;
89
+ return toonText;
90
+ }
91
+
92
+ function applyToonCompression(payload, settings = {}, options = {}) {
93
+ const cfg = normaliseSettings(settings);
94
+ const stats = {
95
+ enabled: cfg.enabled,
96
+ available: true,
97
+ convertedCount: 0,
98
+ candidateCount: 0,
99
+ skippedBySize: 0,
100
+ skippedByShape: 0,
101
+ skippedByParse: 0,
102
+ failureCount: 0,
103
+ originalBytes: 0,
104
+ compressedBytes: 0,
105
+ };
106
+
107
+ if (!cfg.enabled) return { payload, stats };
108
+ if (!payload || !Array.isArray(payload.messages) || payload.messages.length === 0) {
109
+ return { payload, stats };
110
+ }
111
+
112
+ const encodeFn = resolveEncodeFn(options.encode);
113
+ if (typeof encodeFn !== "function") {
114
+ stats.available = false;
115
+ const err = cachedLoadError ?? new Error("TOON encoder unavailable");
116
+ if (!cfg.failOpen) throw err;
117
+ if (!warnedMissingDependency) {
118
+ logger.warn(
119
+ { error: err.message },
120
+ "TOON enabled but encoder dependency is unavailable; falling back to JSON",
121
+ );
122
+ warnedMissingDependency = true;
123
+ }
124
+ return { payload, stats };
125
+ }
126
+
127
+ for (const message of payload.messages) {
128
+ if (!message || typeof message !== "object") continue;
129
+ if (message.role === "tool") continue; // Never mutate machine-executed protocol payloads
130
+ try {
131
+ if (typeof message.content === "string") {
132
+ message.content = compressStringContent(message.content, cfg, encodeFn, stats);
133
+ continue;
134
+ }
135
+
136
+ if (!Array.isArray(message.content)) continue;
137
+ for (const block of message.content) {
138
+ if (!block || typeof block !== "object") continue;
139
+
140
+ // Keep protocol blocks untouched. Only compress user-language text fields.
141
+ if (block.type === "text" && typeof block.text === "string") {
142
+ block.text = compressStringContent(block.text, cfg, encodeFn, stats);
143
+ continue;
144
+ }
145
+
146
+ if (block.type === "input_text" && typeof block.input_text === "string") {
147
+ block.input_text = compressStringContent(block.input_text, cfg, encodeFn, stats);
148
+ }
149
+ }
150
+ } catch (err) {
151
+ stats.failureCount += 1;
152
+ if (!cfg.failOpen) throw err;
153
+ }
154
+ }
155
+
156
+ if (cfg.logStats && stats.convertedCount > 0) {
157
+ logger.info(
158
+ {
159
+ convertedCount: stats.convertedCount,
160
+ candidateCount: stats.candidateCount,
161
+ originalBytes: stats.originalBytes,
162
+ compressedBytes: stats.compressedBytes,
163
+ },
164
+ "TOON compression applied to message context",
165
+ );
166
+ }
167
+
168
+ return { payload, stats };
169
+ }
170
+
171
+ module.exports = {
172
+ applyToonCompression,
173
+ };
@@ -5,12 +5,17 @@
5
5
  * Provides automatic container creation, health checking, and graceful shutdown.
6
6
  */
7
7
 
8
- const Docker = require("dockerode");
8
+ let Docker;
9
+ try {
10
+ Docker = require("dockerode");
11
+ } catch {
12
+ Docker = null;
13
+ }
9
14
  const logger = require("../logger");
10
15
  const config = require("../config");
11
16
 
12
- // Initialize Docker client
13
- const docker = new Docker();
17
+ // Initialize Docker client (only if dockerode is available)
18
+ const docker = Docker ? new Docker() : null;
14
19
 
15
20
  // Launcher state
16
21
  let containerInstance = null;
@@ -1,4 +1,6 @@
1
1
  const pino = require("pino");
2
+ const fs = require("fs");
3
+ const path = require("path");
2
4
  const config = require("../config");
3
5
  const { createOversizedErrorStream } = require("./oversized-error-stream");
4
6
 
@@ -64,6 +66,27 @@ streams.push({
64
66
  : process.stdout,
65
67
  });
66
68
 
69
+ // File rotation stream (if enabled via LOG_FILE_ENABLED=true)
70
+ if (config.logger.file?.enabled) {
71
+ const fileConfig = config.logger.file;
72
+ // Ensure log directory exists
73
+ const logDir = path.dirname(fileConfig.path);
74
+ fs.mkdirSync(logDir, { recursive: true });
75
+
76
+ streams.push({
77
+ level: fileConfig.level,
78
+ stream: pino.transport({
79
+ target: "pino-roll",
80
+ options: {
81
+ file: fileConfig.path,
82
+ frequency: fileConfig.frequency,
83
+ limit: { count: fileConfig.maxFiles },
84
+ mkdir: true,
85
+ },
86
+ }),
87
+ });
88
+ }
89
+
67
90
  // Oversized error stream (if enabled)
68
91
  if (config.oversizedErrorLogging?.enabled) {
69
92
  streams.push({