lynkr 9.14.16 → 9.14.18

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/src/api/router.js CHANGED
@@ -131,8 +131,12 @@ async function pickTierByIntent(body) {
131
131
  const textBearingMsgs = allUserMsgs.filter(
132
132
  (m) => extractCleanUserText({ messages: [m] })
133
133
  );
134
- const windowUserMsgs = (textBearingMsgs.length > 0 ? textBearingMsgs : allUserMsgs)
134
+ let windowUserMsgs = (textBearingMsgs.length > 0 ? textBearingMsgs : allUserMsgs)
135
135
  .slice(-N); // chronological, oldest-first
136
+ try {
137
+ const _hAsk = require("../routing/harness-envelope").harnessAskFromPayload(body);
138
+ if (_hAsk?.text) windowUserMsgs = [{ role: 'user', content: _hAsk.text }];
139
+ } catch { /* fall back to the sliding window */ }
136
140
 
137
141
  // WS3 — we USED to slice tools to 3 here so Claude Code's 11 baseline
138
142
  // tools didn't inflate the agentic detector's tool-count signal. That
@@ -284,6 +288,21 @@ async function pickTierByIntent(body) {
284
288
  perMsg: perMsgScores,
285
289
  }, "[OAuthIntent] window scoring decision");
286
290
 
291
+ // Decision engine must see the ORIGINAL request (harness preamble, tools,
292
+ // session context), not the cleaned single message the window loop scored —
293
+ // the per-message pass strips the envelope the harness signal keys on.
294
+ let _engineOnBody = d.engine || null;
295
+ try {
296
+ const _dec = require("../routing/decisions");
297
+ const _sid = body?._sessionId || null;
298
+ _engineOnBody = await _dec.evaluate({
299
+ payload: body, analysis: d.analysis || {}, risk: d.risk || null, agenticResult: d.agenticResult || null,
300
+ legacy: { tier: d.tier || null, provider: d.provider, model: d.model || null },
301
+ sessionId: _sid, prevTurns: _sid ? require("../routing/outcomes").ring(_sid) : [],
302
+ });
303
+ } catch (err) {
304
+ logger.debug({ err: err.message }, "[OAuthIntent] decision engine on body failed — keeping per-message result");
305
+ }
287
306
  return {
288
307
  tier: d.tier || null,
289
308
  provider: d.provider,
@@ -307,6 +326,7 @@ async function pickTierByIntent(body) {
307
326
  // Underscored: stripped at the outbound chokepoint with every other
308
327
  // internal field, never leaks upstream or to headers.
309
328
  _jev: (d.analysis && d.analysis.jev) || d.jev || null,
329
+ _engine: _engineOnBody,
310
330
  // WS5: feedback path needs the bandit context vector (to call
311
331
  // bandit.update with the same features the arm was scored on) and the
312
332
  // query embedding (to add conclusive-quality outcomes to kNN). Both
@@ -1192,6 +1212,21 @@ router.post("/v1/messages", rateLimiter, async (req, res, next) => {
1192
1212
  // assignment is a no-op when the value already matches.
1193
1213
  if (req.sessionId && !req.body._sessionId) {
1194
1214
  req.body._sessionId = req.sessionId;
1215
+ // Turn-outcome attribution: classify what happened after the previous
1216
+ // turn of this session from the evidence this request carries.
1217
+ try {
1218
+ const _outcomes = require("../routing/outcomes");
1219
+ const _tlm = require("../routing/telemetry");
1220
+ const _prevRow = typeof _tlm.lastForSession === 'function' ? _tlm.lastForSession(req.sessionId) : null;
1221
+ const _prevRecord = _prevRow ? {
1222
+ statusCode: _prevRow.status_code, errorType: _prevRow.error_type, tier: _prevRow.tier, servedModel: _prevRow.model,
1223
+ failover: /fallback/i.test(String(_prevRow.routing_method || '')) || !!_prevRow.was_fallback, tierFallback: !!_prevRow.was_fallback,
1224
+ } : null;
1225
+ const _po = _outcomes.classifyPrevious({ sessionId: req.sessionId, payload: req.body, prevRecord: _prevRecord });
1226
+ if (_po) req.body._prevOutcome = _po;
1227
+ } catch (err) {
1228
+ logger.debug({ err: err.message }, '[Outcomes] classification failed (ignored)');
1229
+ }
1195
1230
  }
1196
1231
 
1197
1232
  // TaskBand — fold the thread (minus the current ask) into a task ledger
@@ -1422,7 +1457,7 @@ router.post("/v1/messages", rateLimiter, async (req, res, next) => {
1422
1457
  // reached the risk classifier. Require a detected client profile
1423
1458
  // (harness UA / tool fingerprint) before treating tool-less traffic as
1424
1459
  // side traffic; a suggestion-mode tag is harness evidence by itself.
1425
- const isKnownHarness = !!req.body?._clientProfile;
1460
+ const isKnownHarness = !!req.body?._clientProfile && req.body._clientProfile.toolless !== true;
1426
1461
  // Signal 1 — message-count regression. Real turns grow the transcript
1427
1462
  // monotonically; a harness replay (title-gen, recap, summary) truncates
1428
1463
  // the history down to the wrapper prompt. If this payload has fewer
@@ -1540,11 +1575,12 @@ router.post("/v1/messages", rateLimiter, async (req, res, next) => {
1540
1575
  let _pinForceBypass = null;
1541
1576
  if (!sideTier && pinCheck.serve && pinCheck.reason === 'guards_passed' && !isSideRequest) {
1542
1577
  try {
1543
- const _probe = { messages: [{ role: 'user', content: _lastUserAskClean || '' }] };
1578
+ const _harnessAskForPin = require("../routing/harness-envelope").harnessAskFromPayload(req.body);
1579
+ const _probe = { messages: [{ role: 'user', content: (_harnessAskForPin ? _harnessAskForPin.text : _lastUserAskClean) || '' }] };
1544
1580
  const ca = require("../routing/complexity-analyzer");
1545
1581
  if (_lastUserText && ca.shouldForceReasoning(_probe)) _pinForceBypass = 'force_reasoning';
1546
1582
  else if (_lastUserText && ca.shouldForceCloud(_probe)) _pinForceBypass = 'force_cloud';
1547
- else if (_lastUserText) {
1583
+ else if (_lastUserText && process.env.RISK_TIER_ESCALATION !== 'false') {
1548
1584
  const _pinRisk = analyzeRisk(_probe);
1549
1585
  if (_pinRisk?.level === 'high') _pinForceBypass = 'risk_high';
1550
1586
  }
@@ -2042,6 +2078,16 @@ router.post("/v1/messages", rateLimiter, async (req, res, next) => {
2042
2078
  // WS4 — propensity + candidates land on every telemetry row so downstream
2043
2079
  // off-policy evaluation can score any counterfactual policy from logs.
2044
2080
  if (tier.propensity != null) req.body._propensity = tier.propensity;
2081
+ if (tier._jev && typeof tier._jev === 'object') req.body._jev = tier._jev;
2082
+ // Declarative decision engine result + per-decision effort travel with the body.
2083
+ const _eng = tier._engine || tier.engine || null;
2084
+ if (_eng) {
2085
+ req.body._engine = { decision: _eng.decision, tier: _eng.tier, mode: _eng.mode, effort: _eng.effort, hosts: _eng.hosts, agreesWithLegacy: _eng.agreesWithLegacy, cascade: _eng.cascade || null };
2086
+ // Effort is a SERVING change: apply it only when the engine is allowed
2087
+ // to serve (enforce mode) or when its tier is the one actually served.
2088
+ if (_eng.effort && (_eng.mode === 'enforce' || _eng.tier === tier.tier)) req.body._effort = _eng.effort;
2089
+ }
2090
+ if (pinCheck && pinCheck.gate) req.body._gate = { action: pinCheck.gate.action, reason: pinCheck.gate.reason, target: pinCheck.gate.target, enforced: pinCheck.gate.enforced, streak: pinCheck.gate.streak };
2045
2091
  if (tier.candidates) req.body._candidates = tier.candidates;
2046
2092
  // WS5 — bandit context vector + query embedding for the feedback loop.
2047
2093
  // All three are underscored; `_stripInternalFields` scrubs them before
@@ -2185,6 +2231,9 @@ router.post("/v1/messages", rateLimiter, async (req, res, next) => {
2185
2231
  };
2186
2232
 
2187
2233
  const routingHeaders = getRoutingHeaders(preRouteDecision);
2234
+ if (process.env.LYNKR_DECISION_HEADERS === 'true' && req._intentTier?._engine) {
2235
+ try { Object.assign(routingHeaders, require("../routing/decisions").headerSummary(req._intentTier._engine)); } catch { /* headers are best-effort */ }
2236
+ }
2188
2237
 
2189
2238
  // Build the interaction block once. It travels in headers always
2190
2239
  // (X-Lynkr-Interaction-* derived fields) and optionally into the
@@ -29,7 +29,9 @@ async function generateOllamaEmbedding(text) {
29
29
  });
30
30
 
31
31
  if (!response.ok) {
32
- throw new Error(`Ollama embedding failed: ${response.status} ${response.statusText}`);
32
+ let body = '';
33
+ try { body = (await response.text()).slice(0, 300); } catch { /* ignore */ }
34
+ throw new Error(`Ollama embedding failed: ${response.status} ${response.statusText}${body ? ' — ' + body : ''}`);
33
35
  }
34
36
 
35
37
  const data = await response.json();
@@ -178,6 +180,10 @@ function _noteRecovery(providerName) {
178
180
  // absorbs the blip; a provider that is actually down fails twice and
179
181
  // degrades exactly as before.
180
182
  const TRANSIENT_RETRY_DELAY_MS = 1500;
183
+ const _INPUT_LENGTH_RE = /context length|input length|too long|exceeds? (the )?(maximum|context)|maximum context/i;
184
+ function _isInputLengthError(err) {
185
+ return _INPUT_LENGTH_RE.test(String(err?.message || err || ''));
186
+ }
181
187
 
182
188
  function _wrapProvider(providerName, providerFn) {
183
189
  return async (text) => {
@@ -196,6 +202,20 @@ function _wrapProvider(providerName, providerFn) {
196
202
  _noteRecovery(providerName);
197
203
  return result;
198
204
  } catch (firstErr) {
205
+ if (_isInputLengthError(firstErr)) {
206
+ try {
207
+ const result = await providerFn(text.slice(0, Math.max(256, Math.floor(text.length / 2))));
208
+ logger.debug({ provider: providerName, chars: text.length },
209
+ '[Embeddings] Input exceeded model context — embedded truncated half instead');
210
+ return result;
211
+ } catch (lenErr) {
212
+ logger.warn({ provider: providerName, error: lenErr?.message, chars: text.length },
213
+ '[Embeddings] Input-length rejection persisted after halving — hash fallback for this request only');
214
+ fallbackCount += 1;
215
+ if (STRICT) throw lenErr;
216
+ return generateHashEmbedding(text);
217
+ }
218
+ }
199
219
  // Already-degraded providers get no second chance (this attempt WAS the
200
220
  // post-cooldown probe); healthy ones earn one retry before the flip.
201
221
  if (embeddingProviderAvailable !== false) {
@@ -282,8 +302,7 @@ async function generateEmbedding(text) {
282
302
  throw new Error('Cannot generate embedding for empty text');
283
303
  }
284
304
 
285
- // Truncate very long text (most embedding models have limits)
286
- const maxLength = 8000;
305
+ const maxLength = Number.parseInt(process.env.LYNKR_EMBEDDINGS_MAX_CHARS, 10) || 5000;
287
306
  const truncated = text.length > maxLength ? text.substring(0, maxLength) : text;
288
307
 
289
308
  const embedFn = getEmbeddingFunction();
@@ -31,7 +31,7 @@
31
31
  * @module clients/cursor-utils
32
32
  */
33
33
 
34
- const { execFile, execSync } = require("node:child_process");
34
+ const { execFile } = require("node:child_process");
35
35
  const crypto = require("node:crypto");
36
36
  const fs = require("node:fs");
37
37
  const os = require("node:os");