lynkr 9.9.0 → 9.10.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 (73) hide show
  1. package/README.md +92 -23
  2. package/bin/cli.js +13 -7
  3. package/bin/lynkr-init.js +34 -12
  4. package/bin/lynkr-reset.js +71 -0
  5. package/config/model-tiers.json +199 -52
  6. package/package.json +3 -3
  7. package/scripts/build-eval-set.js +256 -0
  8. package/scripts/mine-difficulty-anchors.js +288 -0
  9. package/scripts/validate-difficulty-classifier.js +144 -0
  10. package/scripts/validate-intent-anchors.js +186 -0
  11. package/src/api/providers-handler.js +0 -1
  12. package/src/api/router.js +292 -160
  13. package/src/clients/databricks.js +95 -6
  14. package/src/config/index.js +3 -43
  15. package/src/context/tool-result-compressor.js +883 -40
  16. package/src/orchestrator/index.js +117 -1235
  17. package/src/orchestrator/passthrough-stream.js +382 -0
  18. package/src/orchestrator/sse-transformer.js +408 -0
  19. package/src/routing/affinity-store.js +17 -3
  20. package/src/routing/classifier-setup.js +207 -0
  21. package/src/routing/complexity-analyzer.js +40 -4
  22. package/src/routing/difficulty-classifier.js +261 -0
  23. package/src/routing/index.js +70 -7
  24. package/src/routing/intent-score.js +132 -12
  25. package/src/routing/session-affinity.js +8 -1
  26. package/src/routing/side-channel-detector.js +103 -0
  27. package/src/server.js +20 -46
  28. package/src/agents/context-manager.js +0 -236
  29. package/src/agents/decomposition/dispatcher.js +0 -185
  30. package/src/agents/decomposition/gate.js +0 -136
  31. package/src/agents/decomposition/index.js +0 -183
  32. package/src/agents/decomposition/model-call.js +0 -75
  33. package/src/agents/decomposition/planner.js +0 -223
  34. package/src/agents/decomposition/synthesizer.js +0 -89
  35. package/src/agents/decomposition/telemetry.js +0 -55
  36. package/src/agents/definitions/loader.js +0 -653
  37. package/src/agents/executor.js +0 -457
  38. package/src/agents/index.js +0 -165
  39. package/src/agents/parallel-coordinator.js +0 -68
  40. package/src/agents/reflector.js +0 -331
  41. package/src/agents/skillbook.js +0 -331
  42. package/src/agents/store.js +0 -259
  43. package/src/edits/index.js +0 -171
  44. package/src/indexer/babel-parser.js +0 -213
  45. package/src/indexer/index.js +0 -1629
  46. package/src/indexer/navigation/index.js +0 -32
  47. package/src/indexer/navigation/providers/treeSitter.js +0 -36
  48. package/src/indexer/parser.js +0 -443
  49. package/src/tasks/store.js +0 -349
  50. package/src/tests/coverage.js +0 -173
  51. package/src/tests/index.js +0 -171
  52. package/src/tests/store.js +0 -213
  53. package/src/tools/agent-task.js +0 -145
  54. package/src/tools/code-mode.js +0 -304
  55. package/src/tools/decompose.js +0 -91
  56. package/src/tools/edits.js +0 -94
  57. package/src/tools/execution.js +0 -171
  58. package/src/tools/git.js +0 -1346
  59. package/src/tools/index.js +0 -306
  60. package/src/tools/indexer.js +0 -360
  61. package/src/tools/lazy-loader.js +0 -366
  62. package/src/tools/mcp-remote.js +0 -88
  63. package/src/tools/mcp.js +0 -116
  64. package/src/tools/process.js +0 -167
  65. package/src/tools/smart-selection.js +0 -180
  66. package/src/tools/stubs.js +0 -55
  67. package/src/tools/tasks.js +0 -260
  68. package/src/tools/tests.js +0 -132
  69. package/src/tools/tinyfish.js +0 -358
  70. package/src/tools/truncate.js +0 -106
  71. package/src/tools/web-client.js +0 -71
  72. package/src/tools/web.js +0 -415
  73. package/src/tools/workspace.js +0 -204
@@ -299,8 +299,16 @@ async function invokeAzureAnthropic(body, incomingHeaders = {}) {
299
299
  }
300
300
 
301
301
  // OAuth passthrough: prefer incoming Bearer token (Claude Pro/Max subscription)
302
- // over a configured API key.
303
- const incomingAuth = incomingHeaders?.authorization || incomingHeaders?.Authorization;
302
+ // over a configured API key — but ONLY when it is actually an Anthropic
303
+ // token (sk-ant-*). OpenAI-shape clients (Codex, goose) send their own
304
+ // provider's Bearer, and forwarding that to api.anthropic.com earns a
305
+ // guaranteed 401 "Invalid bearer token" (live incident 2026-07-21, Codex
306
+ // via tier fallback).
307
+ let incomingAuth = incomingHeaders?.authorization || incomingHeaders?.Authorization;
308
+ if (incomingAuth && incomingAuth.startsWith("Bearer ") && !incomingAuth.slice("Bearer ".length).startsWith("sk-ant-")) {
309
+ logger.debug({ tokenShape: incomingAuth.slice(7, 15) + "…" }, "Azure Anthropic: ignoring non-Anthropic bearer token from client");
310
+ incomingAuth = null;
311
+ }
304
312
 
305
313
  // Headers Anthropic uses to verify client identity for subscription OAuth tokens.
306
314
  // If we strip these, Anthropic returns 429 rate_limit_error with no rate-limit
@@ -1374,13 +1382,37 @@ async function invokeLlamaCpp(body, incomingHeaders = {}) {
1374
1382
  headers["Authorization"] = `Bearer ${config.llamacpp.apiKey}`;
1375
1383
  }
1376
1384
 
1377
- const messages = convertAnthropicMessagesToOpenRouter(body.messages || []);
1385
+ let messages = convertAnthropicMessagesToOpenRouter(body.messages || []);
1378
1386
 
1379
1387
  if (body.system) {
1380
1388
  messages.unshift({ role: "system", content: body.system });
1381
1389
  }
1382
1390
 
1383
- // FIX: Deduplicate consecutive messages with same role (llama.cpp rejects this)
1391
+ // Fold EVERY system-role message into one leading system message. Claude
1392
+ // Code 2.1.216+ sends system-role entries inside `messages` (a leading one
1393
+ // plus a trailing billing-header-tagged one), and strict Jinja chat
1394
+ // templates (Qwen-family, e.g. Ornith) raise "System message must be at
1395
+ // the beginning" for any non-leading system message. Folding preserves
1396
+ // the content the old consecutive-dedup used to silently DROP.
1397
+ const systemParts = [];
1398
+ const nonSystem = [];
1399
+ for (const msg of messages) {
1400
+ if (msg.role === "system") {
1401
+ const text = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
1402
+ if (text && text.trim()) systemParts.push(text);
1403
+ } else {
1404
+ nonSystem.push(msg);
1405
+ }
1406
+ }
1407
+ if (systemParts.length > 0) {
1408
+ nonSystem.unshift({ role: "system", content: systemParts.join("\n\n") });
1409
+ if (systemParts.length > 1) {
1410
+ logger.debug({ folded: systemParts.length }, "llama.cpp: folded system messages into one leading system message");
1411
+ }
1412
+ }
1413
+ messages = nonSystem;
1414
+
1415
+ // Deduplicate consecutive messages with same role (llama.cpp rejects this)
1384
1416
  const deduplicated = [];
1385
1417
  let lastRole = null;
1386
1418
  for (const msg of messages) {
@@ -1460,7 +1492,20 @@ async function invokeLlamaCpp(body, incomingHeaders = {}) {
1460
1492
  }))
1461
1493
  }, "=== LLAMA.CPP REQUEST ===");
1462
1494
 
1463
- return performJsonRequest(endpoint, { headers, body: llamacppBody }, "llama.cpp");
1495
+ const result = await performJsonRequest(endpoint, { headers, body: llamacppBody }, "llama.cpp");
1496
+
1497
+ // Context overflow is a CAPACITY error, not a request error: the local
1498
+ // model's slot is simply too small for this conversation (llama-server
1499
+ // splits -c across --parallel slots). Returning the 400 as-is fails the
1500
+ // request AND records a provider "success" (nothing threw), so the
1501
+ // tier-fallback ladder never rescues it. Throw instead — same pattern as
1502
+ // _throwIfOllamaError — so invokeModel escalates to a bigger-context tier.
1503
+ const _llamaErrMsg = result?.json?.error?.message || "";
1504
+ if (!result?.ok && /exceeds the available context size/i.test(_llamaErrMsg)) {
1505
+ throw new Error(`llama.cpp context overflow: ${_llamaErrMsg}`);
1506
+ }
1507
+
1508
+ return result;
1464
1509
  }
1465
1510
 
1466
1511
  async function invokeLMStudio(body, incomingHeaders = {}) {
@@ -2681,7 +2726,13 @@ async function invokeModel(body, options = {}) {
2681
2726
  _queryText: body._queryText ?? null,
2682
2727
  }
2683
2728
  : await determineProviderSmart(body, { workspace, tenantPolicy });
2684
- const initialProvider = routingResult.provider;
2729
+ // Canonicalize the provider name ONCE. Tier configs write z.ai as "z-ai";
2730
+ // every dispatch case, breaker, and health key below uses "zai". Without
2731
+ // this the dispatch chain falls through to the Databricks default — whose
2732
+ // base is a deliberate fail-fast dummy port in tier-routing setups — so
2733
+ // every "z-ai" tier request died with fetch's bad-port blocklist while
2734
+ // z.ai itself was perfectly healthy (live incident 2026-07-21/22).
2735
+ const initialProvider = routingResult.provider === "z-ai" ? "zai" : routingResult.provider;
2685
2736
  const tierSelectedModel = routingResult.model;
2686
2737
 
2687
2738
  // Inject tier-selected model into body so provider functions can use it
@@ -2984,6 +3035,21 @@ async function invokeModel(body, options = {}) {
2984
3035
  healthTracker.recordFailure(initialProvider, err, err.status);
2985
3036
  getLatencyTracker().record(initialProvider, routingDecision?.model, failLatency);
2986
3037
 
3038
+ // undici wraps every network failure as "TypeError: fetch failed" — the
3039
+ // actionable detail (ENOTFOUND vs ECONNRESET vs ETIMEDOUT vs TLS) lives
3040
+ // in err.cause. Without this line the z-ai breaker trips on repeated
3041
+ // "fetch failed" with nothing to diagnose (live incidents 2026-07-21/22:
3042
+ // ~5ms failures from a long-lived process while fresh processes succeed).
3043
+ logger.warn({
3044
+ provider: initialProvider,
3045
+ model: routingDecision?.model ?? null,
3046
+ error: err.message,
3047
+ causeCode: err.cause?.code ?? null,
3048
+ causeMessage: err.cause?.message ?? null,
3049
+ causeErrors: Array.isArray(err.cause?.errors) ? err.cause.errors.map((e) => e.code || e.message).slice(0, 4) : null,
3050
+ failLatencyMs: failLatency,
3051
+ }, "Provider invocation failed");
3052
+
2987
3053
  // Tier-aware escalate-then-demote fallback (TIER_FALLBACK_ENABLED).
2988
3054
  // On failure, try a MORE capable tier first (climb toward REASONING); only
2989
3055
  // if every higher tier is unavailable do we fall downward to SIMPLE/local.
@@ -3011,9 +3077,29 @@ async function invokeModel(body, options = {}) {
3011
3077
  _cascadeInner: true,
3012
3078
  workspace,
3013
3079
  tenantPolicy,
3080
+ // Forward the client's headers: azure-anthropic in wrap mode
3081
+ // authenticates with the incoming OAuth Bearer token, so a
3082
+ // fallback attempt without headers is guaranteed to fail with
3083
+ // "requires authentication".
3084
+ headers: incomingHeaders,
3014
3085
  }
3015
3086
  );
3016
3087
 
3088
+ // A candidate that answers with an error is a FAILED candidate,
3089
+ // not a serve. Provider clients return {ok:false} on 4xx instead
3090
+ // of throwing, and serving that here hands the client a 401/403
3091
+ // while healthier rungs below never get tried (live incident
3092
+ // 2026-07-21: Codex request served azure-anthropic's 401 while
3093
+ // llamacpp sat idle one rung down).
3094
+ if (attempt?.ok === false) {
3095
+ logger.warn({
3096
+ toProvider: cand.provider,
3097
+ status: attempt.status,
3098
+ error: attempt.json?.error?.message || attempt.text?.slice(0, 120) || null,
3099
+ }, "[TierFallback] Candidate answered with an error response, trying next");
3100
+ continue;
3101
+ }
3102
+
3017
3103
  metricsCollector.recordFallbackAttempt(initialProvider, cand.provider, "tier_fallback");
3018
3104
  logger.warn({
3019
3105
  servedTier: cand.tier,
@@ -3370,6 +3456,9 @@ function destroyHttpAgents() {
3370
3456
 
3371
3457
  module.exports = {
3372
3458
  invokeModel,
3459
+ invokeAzureAnthropic,
3460
+ invokeZai,
3461
+ invokeOllama,
3373
3462
  stripLynkrBadges,
3374
3463
  destroyHttpAgents,
3375
3464
  normalizeBodyForConverse,
@@ -179,14 +179,6 @@ if (!SUPPORTED_MODEL_PROVIDERS.has(rawFallbackProvider)) {
179
179
 
180
180
  const fallbackProvider = rawFallbackProvider;
181
181
 
182
- // Tool execution mode: server (default), client, or passthrough
183
- const toolExecutionMode = (process.env.TOOL_EXECUTION_MODE ?? "server").toLowerCase();
184
- if (!["server", "client", "passthrough"].includes(toolExecutionMode)) {
185
- throw new Error(
186
- "TOOL_EXECUTION_MODE must be one of: server, client, passthrough (default: server)"
187
- );
188
- }
189
-
190
182
  // Memory system configuration (Titans-inspired long-term memory)
191
183
  const memoryEnabled = process.env.MEMORY_ENABLED !== "false"; // default true
192
184
  const memoryRetrievalLimit = Number.parseInt(process.env.MEMORY_RETRIEVAL_LIMIT ?? "5", 10);
@@ -201,7 +193,6 @@ const memoryDecayHalfLifeDays = Number.parseInt(process.env.MEMORY_DECAY_HALF_LI
201
193
 
202
194
  // Token optimization settings
203
195
  const tokenTrackingEnabled = process.env.TOKEN_TRACKING_ENABLED !== "false"; // default true
204
- const toolTruncationEnabled = process.env.TOOL_TRUNCATION_ENABLED !== "false"; // default true
205
196
  const memoryFormat = (process.env.MEMORY_FORMAT ?? "compact").toLowerCase();
206
197
  const memoryDedupEnabled = process.env.MEMORY_DEDUP_ENABLED !== "false"; // default true
207
198
  const memoryDedupLookback = Number.parseInt(process.env.MEMORY_DEDUP_LOOKBACK ?? "5", 10);
@@ -526,14 +517,9 @@ const testProfiles = parseJson(process.env.WORKSPACE_TEST_PROFILES ?? "", null);
526
517
 
527
518
  // Agents configuration
528
519
  const agentsEnabled = process.env.AGENTS_ENABLED === "true";
529
- const agentsMaxConcurrent = Number.parseInt(process.env.AGENTS_MAX_CONCURRENT ?? "10", 10);
530
- const agentsDefaultModel = process.env.AGENTS_DEFAULT_MODEL ?? "haiku";
531
- const agentsMaxSteps = Number.parseInt(process.env.AGENTS_MAX_STEPS ?? "15", 10);
532
- const agentsTimeout = Number.parseInt(process.env.AGENTS_TIMEOUT ?? "120000", 10);
533
520
 
534
521
  // Task decomposition configuration (opt-in; builds on the agents subsystem).
535
522
  // Single env toggle; everything else is hardcoded below.
536
- const taskDecompositionEnabled = process.env.TASK_DECOMPOSITION_ENABLED === "true";
537
523
 
538
524
  // Tier-aware fallback (escalate-then-demote) is always on (hardcoded).
539
525
 
@@ -667,7 +653,6 @@ var config = {
667
653
  openRouterMaxToolsForRouting,
668
654
  fallbackProvider,
669
655
  },
670
- toolExecutionMode,
671
656
  toolResultCompression: {
672
657
  enabled: true,
673
658
  },
@@ -777,10 +762,6 @@ var config = {
777
762
  manifestPath: sandboxManifestPath,
778
763
  manifestDirs: sandboxManifestDirs,
779
764
  },
780
- codeMode: {
781
- enabled: process.env.CODE_MODE_ENABLED === 'true',
782
- toolListCacheTtl: parseInt(process.env.CODE_MODE_CACHE_TTL, 10) || 60_000,
783
- },
784
765
  },
785
766
  promptCache: {
786
767
  enabled: promptCacheEnabled,
@@ -794,25 +775,10 @@ var config = {
794
775
  ttlMs: Number.parseInt(process.env.SEMANTIC_CACHE_TTL_MS ?? "300000", 10), // 5 minutes (was 1 hour)
795
776
  },
796
777
  agents: {
778
+ // Sole remaining knob: gates the agent-delegation instructions injected
779
+ // into the system prompt (the CLIENT's Task tool). Execution-related
780
+ // agent config died with server-mode tool execution.
797
781
  enabled: agentsEnabled,
798
- maxConcurrent: Number.isNaN(agentsMaxConcurrent) ? 10 : agentsMaxConcurrent,
799
- defaultModel: agentsDefaultModel,
800
- maxSteps: Number.isNaN(agentsMaxSteps) ? 15 : agentsMaxSteps,
801
- timeout: Number.isNaN(agentsTimeout) ? 120000 : agentsTimeout,
802
- },
803
- taskDecomposition: {
804
- enabled: taskDecompositionEnabled, // only env-driven knob (TASK_DECOMPOSITION_ENABLED)
805
- // Hardcoded defaults — tune here if needed.
806
- shadow: false,
807
- planModel: "sonnet",
808
- synthModel: "sonnet",
809
- minConfidence: 0.5,
810
- gate: {
811
- minComplexity: 60,
812
- minTokens: 3000,
813
- maxSubtasks: 6,
814
- minIndependentUnits: 2,
815
- },
816
782
  },
817
783
  tierFallback: {
818
784
  enabled: true, // always on (hardcoded): escalate-then-demote on provider failure; floor = SIMPLE
@@ -851,9 +817,6 @@ var config = {
851
817
  tokenTracking: {
852
818
  enabled: tokenTrackingEnabled,
853
819
  },
854
- toolTruncation: {
855
- enabled: toolTruncationEnabled,
856
- },
857
820
  systemPrompt: {
858
821
  mode: systemPromptMode,
859
822
  toolDescriptions: toolDescriptions,
@@ -1092,9 +1055,6 @@ function reloadConfig() {
1092
1055
  // Graphify
1093
1056
  config.codeGraph.enabled = process.env.CODE_GRAPH_ENABLED === 'true';
1094
1057
 
1095
- // Code Mode
1096
- config.mcp.codeMode.enabled = process.env.CODE_MODE_ENABLED === 'true';
1097
-
1098
1058
  // Log level
1099
1059
  config.logger.level = process.env.LOG_LEVEL ?? "info";
1100
1060