lynkr 9.14.13 → 9.14.15
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/bin/lynkr-init.js +7 -0
- package/package.json +2 -2
- package/scripts/harvest-followup-eval.js +87 -0
- package/src/api/router.js +299 -38
- package/src/clients/databricks.js +8 -0
- package/src/routing/affinity-store.js +136 -1
- package/src/routing/index.js +71 -7
- package/src/routing/intent-score.js +85 -71
- package/src/routing/jev-router.js +128 -25
- package/src/routing/task-ledger.js +443 -0
- package/src/routing/telemetry.js +44 -2
package/bin/lynkr-init.js
CHANGED
|
@@ -595,6 +595,13 @@ async function runInteractive(opts) {
|
|
|
595
595
|
const decay = parseFloat(decayRaw);
|
|
596
596
|
if (!Number.isNaN(decay) && decay > 0 && decay <= 1) env.LYNKR_INTENT_DECAY = String(decay);
|
|
597
597
|
|
|
598
|
+
// Jev routing judge (TypeSafe System One): second opinion on intent
|
|
599
|
+
// difficulty + risk corroboration. Optional — anchor scoring carries
|
|
600
|
+
// routing alone when unset. Written under "# Provider credentials" by
|
|
601
|
+
// the _API_KEY grouping in buildEnvContent.
|
|
602
|
+
const tsKey = await askWithDefault(ask, 'TypeSafe API key for the Jev routing judge (optional — Enter to skip)', process.env.TYPESAFE_API_KEY || '');
|
|
603
|
+
if (tsKey) env.TYPESAFE_API_KEY = tsKey;
|
|
604
|
+
|
|
598
605
|
close();
|
|
599
606
|
console.log('');
|
|
600
607
|
writeEnvFile(buildEnvContent(env, isWrap, tierConfig), opts);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lynkr",
|
|
3
|
-
"version": "9.14.
|
|
3
|
+
"version": "9.14.15",
|
|
4
4
|
"files": [
|
|
5
5
|
"index.js",
|
|
6
6
|
"install.sh",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"start:supervised": "while true; do node index.js 2>&1 | npx pino-pretty --sync; code=$?; echo \"[supervisor] lynkr exited ($code) — restarting in 3s\"; sleep 3; done",
|
|
39
39
|
"lint": "eslint src index.js",
|
|
40
40
|
"test": "npm run test:unit && npm run test:performance",
|
|
41
|
-
"test:unit": "LYNKR_KNN_DIR=/tmp/lynkr-test-knn DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com LOG_FILE_ENABLED=false node --test test/routing.test.js test/hybrid-routing-integration.test.js test/retry-logic.test.js test/sse-transformer.test.js test/passthrough-stream.test.js test/passthrough-mode.test.js test/openrouter-error-resilience.test.js test/format-conversion.test.js test/azure-openai-config.test.js test/azure-openai-format-conversion.test.js test/azure-openai-routing.test.js test/azure-openai-streaming.test.js test/azure-openai-error-resilience.test.js test/azure-openai-integration.test.js test/openai-integration.test.js test/atlas-integration.test.js test/toon-compression.test.js test/gcf-compression.test.js test/llamacpp-integration.test.js test/resilience.test.js test/telemetry-routing.test.js test/memory/store.test.js test/memory/surprise.test.js test/memory/extractor.test.js test/memory/search.test.js test/memory/retriever.test.js test/memory/distiller.test.js test/memory/distiller-freeze.test.js test/memory/wiki.test.js test/memory/skills-cache.test.js test/memory/tencentdb-launcher.test.js test/distill.test.js test/large-payload.test.js test/prompt-cache-injection.test.js test/risk-analyzer.test.js test/interaction-block.test.js test/preflight.test.js test/token-reduction.test.js test/session-affinity.test.js test/cache-state.test.js test/cache-switch-cost.test.js test/lens-recommendations.test.js test/model-registry-cost.test.js test/output-format-guard.test.js test/tier-fallback.test.js test/wrap.test.js test/init.test.js test/tool-call-response-metadata.test.js test/degradation.test.js test/routing-telemetry-columns.test.js test/sticky-routing.test.js test/knn-ambiguous-escalate.test.js test/deescalator.test.js test/client-profiles.test.js test/strip-internal-fields.test.js test/complexity-tool-subtraction.test.js test/bandit.test.js test/routing-propensity.test.js test/reward-pipeline.test.js test/knn-cold-start.test.js test/calibration.test.js test/feedback-loop.test.js test/session-fingerprint.test.js test/side-request-guards.test.js test/verifier.test.js test/intent-score.test.js test/difficulty-classifier.test.js test/classifier-setup.test.js test/usage-stats.test.js test/loop-guard.test.js test/moonshot-model-mapping.test.js test/baidu-model-mapping.test.js test/tenant-policy-ingress-parity.test.js test/decide.test.js test/embeddings-degradation.test.js test/health-probe.test.js test/stuck-detector.test.js test/onnx-embedder.test.js test/ope.test.js test/hierarchical-budget.test.js test/token-rate-limit.test.js test/otel-export.test.js test/mcp-broker.test.js test/compression-budget.test.js test/gpt-utils.test.js test/dedup-observe-only.test.js test/context-window-header.test.js test/token-budget-auto.test.js test/opencode-setup.test.js test/auth-mode-first-party.test.js test/jev-router.test.js test/jev-routing.test.js test/force-patterns.test.js test/upstream-fidelity.test.js test/tool-schema-compression.test.js test/passthrough-route.test.js test/quota-ledger.test.js",
|
|
41
|
+
"test:unit": "LYNKR_KNN_DIR=/tmp/lynkr-test-knn DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com LOG_FILE_ENABLED=false node --test test/routing.test.js test/hybrid-routing-integration.test.js test/retry-logic.test.js test/sse-transformer.test.js test/passthrough-stream.test.js test/passthrough-mode.test.js test/openrouter-error-resilience.test.js test/format-conversion.test.js test/azure-openai-config.test.js test/azure-openai-format-conversion.test.js test/azure-openai-routing.test.js test/azure-openai-streaming.test.js test/azure-openai-error-resilience.test.js test/azure-openai-integration.test.js test/openai-integration.test.js test/atlas-integration.test.js test/toon-compression.test.js test/gcf-compression.test.js test/llamacpp-integration.test.js test/resilience.test.js test/telemetry-routing.test.js test/memory/store.test.js test/memory/surprise.test.js test/memory/extractor.test.js test/memory/search.test.js test/memory/retriever.test.js test/memory/distiller.test.js test/memory/distiller-freeze.test.js test/memory/wiki.test.js test/memory/skills-cache.test.js test/memory/tencentdb-launcher.test.js test/distill.test.js test/large-payload.test.js test/prompt-cache-injection.test.js test/risk-analyzer.test.js test/interaction-block.test.js test/preflight.test.js test/token-reduction.test.js test/session-affinity.test.js test/cache-state.test.js test/cache-switch-cost.test.js test/lens-recommendations.test.js test/model-registry-cost.test.js test/output-format-guard.test.js test/tier-fallback.test.js test/wrap.test.js test/init.test.js test/tool-call-response-metadata.test.js test/degradation.test.js test/routing-telemetry-columns.test.js test/sticky-routing.test.js test/knn-ambiguous-escalate.test.js test/deescalator.test.js test/client-profiles.test.js test/strip-internal-fields.test.js test/complexity-tool-subtraction.test.js test/bandit.test.js test/routing-propensity.test.js test/reward-pipeline.test.js test/knn-cold-start.test.js test/calibration.test.js test/feedback-loop.test.js test/session-fingerprint.test.js test/side-request-guards.test.js test/verifier.test.js test/intent-score.test.js test/difficulty-classifier.test.js test/classifier-setup.test.js test/usage-stats.test.js test/loop-guard.test.js test/moonshot-model-mapping.test.js test/baidu-model-mapping.test.js test/tenant-policy-ingress-parity.test.js test/decide.test.js test/embeddings-degradation.test.js test/health-probe.test.js test/stuck-detector.test.js test/onnx-embedder.test.js test/ope.test.js test/hierarchical-budget.test.js test/token-rate-limit.test.js test/otel-export.test.js test/mcp-broker.test.js test/compression-budget.test.js test/gpt-utils.test.js test/dedup-observe-only.test.js test/context-window-header.test.js test/token-budget-auto.test.js test/opencode-setup.test.js test/auth-mode-first-party.test.js test/task-ledger.test.js test/jev-router.test.js test/jev-routing.test.js test/force-patterns.test.js test/upstream-fidelity.test.js test/tool-schema-compression.test.js test/passthrough-route.test.js test/quota-ledger.test.js",
|
|
42
42
|
"test:memory": "LYNKR_KNN_DIR=/tmp/lynkr-test-knn DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/memory/store.test.js test/memory/surprise.test.js test/memory/extractor.test.js test/memory/search.test.js test/memory/retriever.test.js test/memory/distiller.test.js test/memory/distiller-freeze.test.js test/memory/wiki.test.js test/memory/skills-cache.test.js test/memory/tencentdb-launcher.test.js",
|
|
43
43
|
"test:new-features": "LYNKR_KNN_DIR=/tmp/lynkr-test-knn DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/passthrough-mode.test.js test/openrouter-error-resilience.test.js test/format-conversion.test.js",
|
|
44
44
|
"test:performance": "LYNKR_KNN_DIR=/tmp/lynkr-test-knn DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node test/hybrid-routing-performance.test.js && DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node test/performance-tests.js",
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Harvest short follow-up turns from routing telemetry for the TaskBand
|
|
4
|
+
* continuation-detector eval set.
|
|
5
|
+
*
|
|
6
|
+
* Reads <cwd>/.lynkr/telemetry.db (read-only — never mutates production
|
|
7
|
+
* telemetry), groups rows by session ordered by timestamp, and emits every
|
|
8
|
+
* turn whose request_text is ≤200 chars: short asks are where "do the same
|
|
9
|
+
* here" follow-ups live, and where single-message scoring is blindest.
|
|
10
|
+
* Each line carries up to 3 prior asks from the same session so a labeler
|
|
11
|
+
* can judge continuation vs. new task without replaying the session.
|
|
12
|
+
*
|
|
13
|
+
* Output: data/followup-eval-unlabeled.jsonl (gitignored), one JSON object
|
|
14
|
+
* per line: {session_id, ts, request_text, prior_asks, chosen_tier,
|
|
15
|
+
* complexity_score, label: null} — label is filled in by hand.
|
|
16
|
+
*
|
|
17
|
+
* Usage: node scripts/harvest-followup-eval.js
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require("fs");
|
|
21
|
+
const path = require("path");
|
|
22
|
+
|
|
23
|
+
const MAX_ASK_CHARS = 200;
|
|
24
|
+
const MAX_PRIOR_ASKS = 3;
|
|
25
|
+
const OUT_FILE = path.join(__dirname, "../data/followup-eval-unlabeled.jsonl");
|
|
26
|
+
|
|
27
|
+
let Database;
|
|
28
|
+
try {
|
|
29
|
+
Database = require("better-sqlite3");
|
|
30
|
+
} catch {
|
|
31
|
+
console.error("better-sqlite3 is not installed (it is an optionalDependency) — cannot read telemetry.db.");
|
|
32
|
+
console.error("Install it with: npm install better-sqlite3");
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function main() {
|
|
37
|
+
const dbPath = path.join(process.cwd(), ".lynkr", "telemetry.db");
|
|
38
|
+
if (!fs.existsSync(dbPath)) {
|
|
39
|
+
console.error(`No telemetry database at ${dbPath} — run the proxy from this directory first to collect telemetry.`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const db = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
44
|
+
|
|
45
|
+
const rows = db
|
|
46
|
+
.prepare(
|
|
47
|
+
`SELECT session_id, timestamp, request_text, tier, complexity_score
|
|
48
|
+
FROM routing_telemetry
|
|
49
|
+
WHERE session_id IS NOT NULL AND request_text IS NOT NULL AND request_text != ''
|
|
50
|
+
ORDER BY session_id, timestamp ASC`
|
|
51
|
+
)
|
|
52
|
+
.all();
|
|
53
|
+
db.close();
|
|
54
|
+
|
|
55
|
+
const out = [];
|
|
56
|
+
let sessions = 0;
|
|
57
|
+
let prevSession = null;
|
|
58
|
+
let priorAsks = [];
|
|
59
|
+
|
|
60
|
+
for (const row of rows) {
|
|
61
|
+
if (row.session_id !== prevSession) {
|
|
62
|
+
prevSession = row.session_id;
|
|
63
|
+
priorAsks = [];
|
|
64
|
+
sessions++;
|
|
65
|
+
}
|
|
66
|
+
const text = String(row.request_text);
|
|
67
|
+
if (text.length <= MAX_ASK_CHARS) {
|
|
68
|
+
out.push(JSON.stringify({
|
|
69
|
+
session_id: row.session_id,
|
|
70
|
+
ts: row.timestamp,
|
|
71
|
+
request_text: text,
|
|
72
|
+
prior_asks: priorAsks.slice(-MAX_PRIOR_ASKS),
|
|
73
|
+
chosen_tier: row.tier ?? null,
|
|
74
|
+
complexity_score: row.complexity_score ?? null,
|
|
75
|
+
label: null,
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
priorAsks.push(text);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fs.mkdirSync(path.dirname(OUT_FILE), { recursive: true });
|
|
82
|
+
fs.writeFileSync(OUT_FILE, out.length ? out.join("\n") + "\n" : "");
|
|
83
|
+
console.log(`Scanned ${rows.length} rows across ${sessions} sessions.`);
|
|
84
|
+
console.log(`Wrote ${OUT_FILE}: ${out.length} candidate follow-up turns (label: null — awaiting hand labels).`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
main();
|
package/src/api/router.js
CHANGED
|
@@ -44,6 +44,24 @@ const router = express.Router();
|
|
|
44
44
|
|
|
45
45
|
const rateLimiter = createRateLimiter();
|
|
46
46
|
|
|
47
|
+
// TaskBand holdout: a deterministic slice of sessions never receives the
|
|
48
|
+
// continuation floor, so telemetry keeps a running baseline cohort proving
|
|
49
|
+
// floored sessions aren't quietly doing worse. FNV-1a bucketing mirrors
|
|
50
|
+
// deescalator.isHeldOut; the percentage is task-ledger's hardcoded
|
|
51
|
+
// HOLDOUT_PCT (no env knob by design).
|
|
52
|
+
function _taskbandHoldout(sessionKey, pct) {
|
|
53
|
+
if (!sessionKey) return false;
|
|
54
|
+
const cut = Math.round(Math.max(0, Math.min(1, Number(pct) || 0)) * 100);
|
|
55
|
+
if (cut === 0) return false;
|
|
56
|
+
let hash = 0x811c9dc5;
|
|
57
|
+
const s = String(sessionKey);
|
|
58
|
+
for (let i = 0; i < s.length; i++) {
|
|
59
|
+
hash ^= s.charCodeAt(i);
|
|
60
|
+
hash = Math.imul(hash, 0x01000193);
|
|
61
|
+
}
|
|
62
|
+
return ((hash >>> 0) % 100) < cut;
|
|
63
|
+
}
|
|
64
|
+
|
|
47
65
|
/**
|
|
48
66
|
* Decide which tier/provider/model handles an OAuth-subscription request.
|
|
49
67
|
*
|
|
@@ -170,45 +188,25 @@ async function pickTierByIntent(body) {
|
|
|
170
188
|
};
|
|
171
189
|
}
|
|
172
190
|
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
let text = '';
|
|
178
|
-
if (typeof msg.content === 'string') text = msg.content;
|
|
179
|
-
else if (Array.isArray(msg.content)) {
|
|
180
|
-
text = msg.content
|
|
181
|
-
.filter((b) => b?.type === 'text' && typeof b.text === 'string')
|
|
182
|
-
.map((b) => b.text)
|
|
183
|
-
.join(' ');
|
|
184
|
-
}
|
|
185
|
-
return stripReminders(text).replace(/\s+/g, ' ').trim().slice(0, max);
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
// Conversation context for the difficulty classifier: the nearest prior
|
|
189
|
-
// assistant reply and the user turn before it. A bare follow-up ("Who
|
|
190
|
-
// kills him ?") is unclassifiable in isolation — the single-message
|
|
191
|
+
// Task-ledger context for the difficulty judge: a slim stable string
|
|
192
|
+
// (task anchor + last ask) folded from the PREFIX of the thread — the ask
|
|
193
|
+
// being scored never feeds its own context. A bare follow-up ("Who kills
|
|
194
|
+
// him ?") is unclassifiable in isolation — the single-message
|
|
191
195
|
// intentPayload below is context-blind BY DESIGN (envelope invariance),
|
|
192
196
|
// so the context travels as a separate condensed string, never as extra
|
|
193
|
-
// messages that would pollute the anchor score.
|
|
194
|
-
|
|
197
|
+
// messages that would pollute the anchor score. The ledger's prefix memo
|
|
198
|
+
// keeps the per-index derivation O(1) per new ask.
|
|
199
|
+
let _taskLedgerMod = null;
|
|
200
|
+
try { _taskLedgerMod = require("../routing/task-ledger"); } catch { /* context degrades to null */ }
|
|
201
|
+
const _ledgerBefore = (msg) => {
|
|
202
|
+
if (!_taskLedgerMod) return null;
|
|
195
203
|
const idx = messages.indexOf(msg);
|
|
196
204
|
if (idx <= 0) return null;
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (!assistantText && m?.role === 'assistant') assistantText = _condenseText(m);
|
|
202
|
-
else if (assistantText && m?.role === 'user') {
|
|
203
|
-
priorUserText = _condenseText(m);
|
|
204
|
-
if (priorUserText) break;
|
|
205
|
-
}
|
|
205
|
+
try {
|
|
206
|
+
return _taskLedgerMod.deriveTaskLedger(messages.slice(0, idx));
|
|
207
|
+
} catch {
|
|
208
|
+
return null;
|
|
206
209
|
}
|
|
207
|
-
if (!assistantText && !priorUserText) return null;
|
|
208
|
-
const parts = [];
|
|
209
|
-
if (priorUserText) parts.push(`user asked: """${priorUserText}"""`);
|
|
210
|
-
if (assistantText) parts.push(`assistant replied about: """${assistantText}"""`);
|
|
211
|
-
return parts.join(' → ');
|
|
212
210
|
};
|
|
213
211
|
|
|
214
212
|
// Per-message scoring intentionally omits _sessionId so session affinity
|
|
@@ -223,6 +221,11 @@ async function pickTierByIntent(body) {
|
|
|
223
221
|
for (let i = 0; i < windowUserMsgs.length; i++) {
|
|
224
222
|
const age = windowUserMsgs.length - 1 - i; // 0 = latest, length-1 = oldest in window
|
|
225
223
|
const cleaned = cleanMsg(windowUserMsgs[i]);
|
|
224
|
+
const _msgLedger = _ledgerBefore(windowUserMsgs[i]);
|
|
225
|
+
let _msgContext = null;
|
|
226
|
+
try {
|
|
227
|
+
_msgContext = _msgLedger ? _taskLedgerMod.buildTaskContext(_msgLedger) : null;
|
|
228
|
+
} catch { /* context degrades to null */ }
|
|
226
229
|
const intentPayload = {
|
|
227
230
|
messages: cleaned ? [cleaned] : [],
|
|
228
231
|
tools: intentTools,
|
|
@@ -230,10 +233,17 @@ async function pickTierByIntent(body) {
|
|
|
230
233
|
// agentic detector inside determineProviderSmart can subtract the
|
|
231
234
|
// harness's baseline tools during intent scoring too.
|
|
232
235
|
_clientProfile: body?._clientProfile || null,
|
|
233
|
-
// Classifier context (see
|
|
234
|
-
// the outbound chokepoint like every internal field.
|
|
235
|
-
_conversationContext:
|
|
236
|
+
// Classifier context + memo identity (see _ledgerBefore). Underscored
|
|
237
|
+
// — stripped at the outbound chokepoint like every internal field.
|
|
238
|
+
_conversationContext: _msgContext,
|
|
239
|
+
_taskAnchorHash: _msgLedger?.anchorHash || null,
|
|
236
240
|
};
|
|
241
|
+
if (age === 0) {
|
|
242
|
+
// Continuation detection ran ONCE in the handler, on the latest typed
|
|
243
|
+
// ask only — windowed history messages must never carry these.
|
|
244
|
+
if (body?._isContinuation) intentPayload._isContinuation = true;
|
|
245
|
+
if (body?._inheritedFloorTier) intentPayload._inheritedFloorTier = body._inheritedFloorTier;
|
|
246
|
+
}
|
|
237
247
|
try {
|
|
238
248
|
const decision = await determineProviderSmart(intentPayload, {
|
|
239
249
|
workspace: body?._workspace || null,
|
|
@@ -359,7 +369,9 @@ async function handleOauthPassthrough(req, res, opts = {}) {
|
|
|
359
369
|
// Strip Lynkr's internal underscore-prefixed fields (_sessionId,
|
|
360
370
|
// _forceProvider, _tierModel, _tierName, _forcedMethod, _baseTier,
|
|
361
371
|
// _escalationSource, _pinnedRoute, _switchReason, _clientProfile,
|
|
362
|
-
// _workspace, _tenantPolicy, _deadlineMs, _suggestionModeModel
|
|
372
|
+
// _workspace, _tenantPolicy, _deadlineMs, _suggestionModeModel,
|
|
373
|
+
// _conversationContext, _taskAnchorHash, _isContinuation,
|
|
374
|
+
// _inheritedFloorTier, _taskband). Anthropic
|
|
363
375
|
// rejects unknown top-level keys with "Extra inputs are not permitted".
|
|
364
376
|
// The orchestrator's downstream paths whitelist their outbound bodies,
|
|
365
377
|
// but the passthrough sends this body VERBATIM — so we must strip here.
|
|
@@ -664,6 +676,7 @@ async function handleOauthPassthrough(req, res, opts = {}) {
|
|
|
664
676
|
message_count: req.body?.messages?.length || null,
|
|
665
677
|
tool_count: Array.isArray(req.body?.tools) ? req.body.tools.length : 0,
|
|
666
678
|
...tlm.jevFields(tier),
|
|
679
|
+
...tlm.taskbandFields(tier),
|
|
667
680
|
// Inner-decision attribution: the tier label alone can't explain a
|
|
668
681
|
// serve (a COMPLEX label once served Haiku with no trace because the
|
|
669
682
|
// tier object carried a null model). Record the decision's own
|
|
@@ -1180,6 +1193,96 @@ router.post("/v1/messages", rateLimiter, async (req, res, next) => {
|
|
|
1180
1193
|
req.body._sessionId = req.sessionId;
|
|
1181
1194
|
}
|
|
1182
1195
|
|
|
1196
|
+
// TaskBand — fold the thread (minus the current ask) into a task ledger
|
|
1197
|
+
// and run continuation detection ONCE, on the latest typed ask only.
|
|
1198
|
+
// Stamped on the body so pin-drift checks, the per-message intent loop
|
|
1199
|
+
// and the difficulty judge all score from identical inputs. Best-effort
|
|
1200
|
+
// throughout: any failure leaves routing exactly as before TaskBand.
|
|
1201
|
+
let _taskLedger = null;
|
|
1202
|
+
let _taskCont = null;
|
|
1203
|
+
let _taskFloorTier = null; // would-be continuation floor (tier name)
|
|
1204
|
+
let _taskFloorHeldOut = false; // holdout cohort: floor logged, not applied
|
|
1205
|
+
let _taskPreFloorTier = null; // fresh tier before the floor raised it
|
|
1206
|
+
let _continuationFloored = false;
|
|
1207
|
+
let _taskSideRequest = false;
|
|
1208
|
+
let _jevStatsBefore = null;
|
|
1209
|
+
try { _jevStatsBefore = require("../routing/jev-router").getJevCacheStats(); } catch { /* stamp stays null */ }
|
|
1210
|
+
try {
|
|
1211
|
+
const taskLedgerMod = require("../routing/task-ledger");
|
|
1212
|
+
const { cleanUserText } = require("../routing/jev-router");
|
|
1213
|
+
const _msgs = Array.isArray(req.body?.messages) ? req.body.messages : [];
|
|
1214
|
+
let _askIdx = -1;
|
|
1215
|
+
let _askText = null;
|
|
1216
|
+
for (let i = _msgs.length - 1; i >= 0; i--) {
|
|
1217
|
+
if (_msgs[i]?.role !== 'user') continue;
|
|
1218
|
+
const t = cleanUserText(_msgs[i]);
|
|
1219
|
+
if (t) { _askIdx = i; _askText = t; break; }
|
|
1220
|
+
}
|
|
1221
|
+
if (_askIdx > 0) _taskLedger = taskLedgerMod.deriveTaskLedger(_msgs.slice(0, _askIdx));
|
|
1222
|
+
if (_taskLedger) {
|
|
1223
|
+
const _ctx = taskLedgerMod.buildTaskContext(_taskLedger);
|
|
1224
|
+
if (_ctx) req.body._conversationContext = _ctx;
|
|
1225
|
+
if (_taskLedger.anchorHash) req.body._taskAnchorHash = _taskLedger.anchorHash;
|
|
1226
|
+
const _embedFn = async (t) => {
|
|
1227
|
+
try {
|
|
1228
|
+
return await require("../routing/knn-router").getKnnRouter().embed(t);
|
|
1229
|
+
} catch { return null; }
|
|
1230
|
+
};
|
|
1231
|
+
_taskCont = await taskLedgerMod.detectContinuation(_askText, _taskLedger, _embedFn);
|
|
1232
|
+
if (_taskCont?.isContinuation) req.body._isContinuation = true;
|
|
1233
|
+
}
|
|
1234
|
+
const {
|
|
1235
|
+
FLOOR_ENABLED = true,
|
|
1236
|
+
DECAY_TURNS = 2,
|
|
1237
|
+
HOLDOUT_PCT = 0.10,
|
|
1238
|
+
} = taskLedgerMod;
|
|
1239
|
+
if (FLOOR_ENABLED && _taskCont?.isContinuation && _taskLedger?.anchorHash) {
|
|
1240
|
+
const TIER_ORDER = ['SIMPLE', 'MEDIUM', 'COMPLEX', 'REASONING'];
|
|
1241
|
+
let effIdx = null;
|
|
1242
|
+
let coldFallback = false;
|
|
1243
|
+
const _taskState = req.body._sessionId
|
|
1244
|
+
? require("../routing/affinity-store").getTaskState(req.body._sessionId)
|
|
1245
|
+
: null;
|
|
1246
|
+
if (_taskState && _taskState.anchorHash === _taskLedger.anchorHash
|
|
1247
|
+
&& Number.isInteger(_taskState.effectiveBand)) {
|
|
1248
|
+
effIdx = Math.max(0, Math.min(3, _taskState.effectiveBand));
|
|
1249
|
+
} else if (_taskLedger.anchorText) {
|
|
1250
|
+
// COLD fallback: the task predates its state row (restart, TTL,
|
|
1251
|
+
// anchor change). Anchor-only score of the task's first ask,
|
|
1252
|
+
// capped at MEDIUM — an unproven inheritance must not re-derive
|
|
1253
|
+
// an expensive band from a single string.
|
|
1254
|
+
const { scoreIntent } = require("../routing/intent-score");
|
|
1255
|
+
const anchorIntent = await scoreIntent(
|
|
1256
|
+
{ messages: [{ role: 'user', content: _taskLedger.anchorText }] },
|
|
1257
|
+
{ skipClassifier: true },
|
|
1258
|
+
);
|
|
1259
|
+
if (anchorIntent && Number.isFinite(anchorIntent.score)) {
|
|
1260
|
+
const t = getModelTierSelector().getTier(anchorIntent.score);
|
|
1261
|
+
effIdx = Math.min(TIER_ORDER.indexOf(t), TIER_ORDER.indexOf('MEDIUM'));
|
|
1262
|
+
coldFallback = true;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
let floorIdx = null;
|
|
1266
|
+
if (effIdx != null && effIdx >= 0) {
|
|
1267
|
+
if (_taskLedger.open) floorIdx = effIdx;
|
|
1268
|
+
else if ((_taskLedger.turnsSinceClose ?? Infinity) <= DECAY_TURNS) floorIdx = effIdx - 1;
|
|
1269
|
+
}
|
|
1270
|
+
if (floorIdx != null && coldFallback) {
|
|
1271
|
+
floorIdx = Math.min(floorIdx, TIER_ORDER.indexOf('COMPLEX'));
|
|
1272
|
+
}
|
|
1273
|
+
if (floorIdx != null && floorIdx >= 0) {
|
|
1274
|
+
_taskFloorTier = TIER_ORDER[floorIdx];
|
|
1275
|
+
_taskFloorHeldOut = _taskbandHoldout(req.body._sessionId, HOLDOUT_PCT);
|
|
1276
|
+
// Held-out sessions must stay floor-free end to end — the judge's
|
|
1277
|
+
// cap widening counts as an application, so the body stamp is
|
|
1278
|
+
// withheld too, keeping the baseline cohort clean.
|
|
1279
|
+
if (!_taskFloorHeldOut) req.body._inheritedFloorTier = _taskFloorTier;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
} catch (err) {
|
|
1283
|
+
logger.debug({ err: err.message }, '[TaskBand] ledger/continuation derivation failed');
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1183
1286
|
// WS1 — sticky-session reuse. If this session already has a valid pin
|
|
1184
1287
|
// (guards pass, no compaction), skip pickTierByIntent entirely and reuse
|
|
1185
1288
|
// the pinned decision. This is the biggest cost win of WS1: repeat turns
|
|
@@ -1358,6 +1461,9 @@ router.post("/v1/messages", rateLimiter, async (req, res, next) => {
|
|
|
1358
1461
|
|| isBareRequest
|
|
1359
1462
|
|| isFingerprintDrift
|
|
1360
1463
|
|| ((!Array.isArray(req.body?.tools) || req.body.tools.length === 0) && isKnownHarness);
|
|
1464
|
+
// Task state must never see side traffic (hoisted for the upkeep block
|
|
1465
|
+
// at the final-decision site — the model-id-pin branch skips this scope).
|
|
1466
|
+
_taskSideRequest = isSideRequest;
|
|
1361
1467
|
// Side requests short-circuit to the static SIMPLE tier: no pin read
|
|
1362
1468
|
// (a COMPLEX-pinned conversation would burn expensive tokens on
|
|
1363
1469
|
// autocomplete), no pin write, no intent scoring of wrapper text.
|
|
@@ -1521,6 +1627,63 @@ router.post("/v1/messages", rateLimiter, async (req, res, next) => {
|
|
|
1521
1627
|
};
|
|
1522
1628
|
}
|
|
1523
1629
|
}
|
|
1630
|
+
// TaskBand continuation floor — fresh decisions only, and only to
|
|
1631
|
+
// RAISE. Placement is deliberate: after the compaction floor so the
|
|
1632
|
+
// two floors compose (max wins), before the pin write so a floored
|
|
1633
|
+
// tier can never fossilize into a session pin (writeSessionPin also
|
|
1634
|
+
// refuses '+continuation_inherit', mirroring the risk skip).
|
|
1635
|
+
if (!isSideRequest && _taskFloorTier && tier?.tier) {
|
|
1636
|
+
try {
|
|
1637
|
+
const { TIER_DEFINITIONS } = require("../routing/model-tiers");
|
|
1638
|
+
const pri = (t) => TIER_DEFINITIONS[t]?.priority ?? -1;
|
|
1639
|
+
const wouldRaise = pri(tier.tier) < pri(_taskFloorTier);
|
|
1640
|
+
if (_taskFloorHeldOut && wouldRaise) {
|
|
1641
|
+
tier.method = (tier.method || 'tier_config') + '+continuation_holdout';
|
|
1642
|
+
logger.debug({
|
|
1643
|
+
sessionId: pinCheck.sessionId,
|
|
1644
|
+
freshTier: tier.tier,
|
|
1645
|
+
wouldFloorTo: _taskFloorTier,
|
|
1646
|
+
evidence: _taskCont?.evidence || [],
|
|
1647
|
+
}, "OAuth intent — continuation floor held out (baseline cohort)");
|
|
1648
|
+
} else if (wouldRaise) {
|
|
1649
|
+
const flooredSel = getModelTierSelector().selectModel(_taskFloorTier, null);
|
|
1650
|
+
if (flooredSel?.provider) {
|
|
1651
|
+
const _fromTier = tier.tier;
|
|
1652
|
+
const _fromModel = tier.model || null;
|
|
1653
|
+
_taskPreFloorTier = _fromTier;
|
|
1654
|
+
logger.info({
|
|
1655
|
+
sessionId: pinCheck.sessionId,
|
|
1656
|
+
freshTier: _fromTier,
|
|
1657
|
+
flooredTo: _taskFloorTier,
|
|
1658
|
+
evidence: _taskCont?.evidence || [],
|
|
1659
|
+
}, "OAuth intent — continuation inherits task band");
|
|
1660
|
+
tier = {
|
|
1661
|
+
...tier,
|
|
1662
|
+
tier: _taskFloorTier,
|
|
1663
|
+
provider: flooredSel.provider,
|
|
1664
|
+
model: flooredSel.model || null,
|
|
1665
|
+
method: (tier.method || 'tier_config') + '+continuation_inherit',
|
|
1666
|
+
switch_reason: tier.switch_reason || 'continuation_inherit',
|
|
1667
|
+
base_tier: tier.base_tier ?? _fromTier,
|
|
1668
|
+
escalation_source: tier.escalation_source ?? 'continuation_inherit',
|
|
1669
|
+
escalations: [
|
|
1670
|
+
...(Array.isArray(tier.escalations) ? tier.escalations : []),
|
|
1671
|
+
{
|
|
1672
|
+
source: 'continuation_inherit',
|
|
1673
|
+
fromTier: _fromTier,
|
|
1674
|
+
toTier: _taskFloorTier,
|
|
1675
|
+
fromModel: _fromModel,
|
|
1676
|
+
toModel: flooredSel.model || null,
|
|
1677
|
+
},
|
|
1678
|
+
],
|
|
1679
|
+
};
|
|
1680
|
+
_continuationFloored = true;
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
} catch (err) {
|
|
1684
|
+
logger.debug({ err: err.message }, '[TaskBand] continuation floor failed — fresh tier stands');
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1524
1687
|
// Persist the fresh decision so the next turn on this session can
|
|
1525
1688
|
// reuse it. checkSessionPin returned serve=false (or WS1.5 drift fired),
|
|
1526
1689
|
// so either there was no pin, the pin lost a guard (context/vision/
|
|
@@ -1546,6 +1709,104 @@ router.post("/v1/messages", rateLimiter, async (req, res, next) => {
|
|
|
1546
1709
|
tier.switch_reason = 'model_pin_drift';
|
|
1547
1710
|
}
|
|
1548
1711
|
|
|
1712
|
+
// TaskBand telemetry stamp — travels on the decision AND (underscored)
|
|
1713
|
+
// on the body, so the forced-provider path can reconstitute it onto the
|
|
1714
|
+
// routingResult that feeds routing_telemetry.
|
|
1715
|
+
try {
|
|
1716
|
+
let _jevCacheHit = null;
|
|
1717
|
+
if (_jevStatsBefore) {
|
|
1718
|
+
const _jevStatsAfter = require("../routing/jev-router").getJevCacheStats();
|
|
1719
|
+
const _missDelta = _jevStatsAfter.misses - _jevStatsBefore.misses;
|
|
1720
|
+
const _hitDelta = _jevStatsAfter.hits - _jevStatsBefore.hits;
|
|
1721
|
+
// Null means the judge never consulted its cache this request
|
|
1722
|
+
// (pin serve, side frame, classifier skip) — "not measured".
|
|
1723
|
+
_jevCacheHit = _missDelta > 0 ? 0 : (_hitDelta > 0 ? 1 : null);
|
|
1724
|
+
}
|
|
1725
|
+
tier.taskband = {
|
|
1726
|
+
isContinuation: _taskCont?.isContinuation ? 1 : 0,
|
|
1727
|
+
inheritedFloor: _taskFloorTier || null,
|
|
1728
|
+
anchorHash: _taskLedger?.anchorHash || null,
|
|
1729
|
+
jevContext: typeof req.body._conversationContext === 'string'
|
|
1730
|
+
? req.body._conversationContext : null,
|
|
1731
|
+
jevCacheHit: _jevCacheHit,
|
|
1732
|
+
};
|
|
1733
|
+
req.body._taskband = tier.taskband;
|
|
1734
|
+
} catch (err) {
|
|
1735
|
+
logger.debug({ err: err.message }, '[TaskBand] telemetry stamp failed');
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// TaskBand task-state upkeep — once per request, at the final-decision
|
|
1739
|
+
// site, never inside the per-message scoring loop. Side requests never
|
|
1740
|
+
// touch task state. Pin replays never write: a pinned tier keeps no
|
|
1741
|
+
// provenance of how it was chosen (a force-triggered tier pins like any
|
|
1742
|
+
// scored one), so replaying it must not launder into earned band — the
|
|
1743
|
+
// fresh serve that created the pin already merged. Model-id pins are
|
|
1744
|
+
// explicit client choices, not earned difficulty — no writes either.
|
|
1745
|
+
try {
|
|
1746
|
+
const _sessForTask = req.body._sessionId || null;
|
|
1747
|
+
if (_sessForTask && _taskLedger?.anchorHash && !_taskSideRequest) {
|
|
1748
|
+
const store = require("../routing/affinity-store");
|
|
1749
|
+
let AUTO_CLOSE_LOW_TURNS = 3;
|
|
1750
|
+
try {
|
|
1751
|
+
const _tl = require("../routing/task-ledger");
|
|
1752
|
+
if (Number.isInteger(_tl.AUTO_CLOSE_LOW_TURNS)) AUTO_CLOSE_LOW_TURNS = _tl.AUTO_CLOSE_LOW_TURNS;
|
|
1753
|
+
} catch { /* default stands */ }
|
|
1754
|
+
const TIER_ORDER = ['SIMPLE', 'MEDIUM', 'COMPLEX', 'REASONING'];
|
|
1755
|
+
const _method = tier?.method || '';
|
|
1756
|
+
const _openNow = _taskLedger.open ? 1 : 0;
|
|
1757
|
+
const _row = store.getTaskState(_sessForTask);
|
|
1758
|
+
const _anchorMatches = !!(_row && _row.anchorHash === _taskLedger.anchorHash);
|
|
1759
|
+
if (_method !== 'session_pin' && _method !== 'model_id_pin' && _method !== 'fallback') {
|
|
1760
|
+
// Low-streak decay reads the FRESH-scored tier, not the floored
|
|
1761
|
+
// one — otherwise an applied floor could never auto-close.
|
|
1762
|
+
let _freshTier = _taskPreFloorTier || tier?.tier || null;
|
|
1763
|
+
// A jev-override lift rides a cap the inherited floor may have
|
|
1764
|
+
// widened; only the scored base tier is earned band. Merging the
|
|
1765
|
+
// lifted tier would raise the next turn's floor, which widens the
|
|
1766
|
+
// next cap — the ratchet the one-band-up cap exists to break.
|
|
1767
|
+
if (/(^|\+)jev_override(\+|$)/.test(_method)) {
|
|
1768
|
+
const _scoredIdx = TIER_ORDER.indexOf(tier?.base_tier);
|
|
1769
|
+
if (_scoredIdx >= 0 && _scoredIdx < TIER_ORDER.indexOf(_freshTier)) {
|
|
1770
|
+
_freshTier = tier.base_tier;
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
const _freshIdx = TIER_ORDER.indexOf(_freshTier);
|
|
1774
|
+
const _forceOrRisk = /(^|\+)(force|risk)(\+|$)/.test(_method)
|
|
1775
|
+
|| tier?.escalation_source === 'risk';
|
|
1776
|
+
if (_freshIdx >= 0) {
|
|
1777
|
+
let _lowStreak = 0;
|
|
1778
|
+
if (_anchorMatches && Number.isInteger(_row.effectiveBand)
|
|
1779
|
+
&& _freshIdx <= _row.effectiveBand - 2) {
|
|
1780
|
+
_lowStreak = (_row.lowStreak ?? 0) + 1;
|
|
1781
|
+
}
|
|
1782
|
+
if (_lowStreak >= AUTO_CLOSE_LOW_TURNS) {
|
|
1783
|
+
store.clearTaskState(_sessForTask);
|
|
1784
|
+
} else if (!_continuationFloored && !_forceOrRisk) {
|
|
1785
|
+
// MAX-merge on matching anchor, whole-row overwrite on an
|
|
1786
|
+
// anchor change — both live inside setTaskState.
|
|
1787
|
+
store.setTaskState(_sessForTask, {
|
|
1788
|
+
anchorHash: _taskLedger.anchorHash,
|
|
1789
|
+
effectiveBand: _freshIdx,
|
|
1790
|
+
open: _openNow,
|
|
1791
|
+
lowStreak: _lowStreak,
|
|
1792
|
+
});
|
|
1793
|
+
} else if (_anchorMatches) {
|
|
1794
|
+
// Floored/force/risk serves never merge their band, but the
|
|
1795
|
+
// marker-close and the low streak still have to land.
|
|
1796
|
+
store.setTaskState(_sessForTask, {
|
|
1797
|
+
anchorHash: _row.anchorHash,
|
|
1798
|
+
effectiveBand: Number.isInteger(_row.effectiveBand) ? _row.effectiveBand : _freshIdx,
|
|
1799
|
+
open: _row.open && _openNow ? 1 : 0,
|
|
1800
|
+
lowStreak: _lowStreak,
|
|
1801
|
+
});
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
} catch (err) {
|
|
1807
|
+
logger.debug({ err: err.message }, '[TaskBand] task-state upkeep failed');
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1549
1810
|
// Subscription-only fork: byte-for-byte passthrough to api.anthropic.com
|
|
1550
1811
|
// when the picked tier resolves to azure-anthropic — FIRST-PARTY
|
|
1551
1812
|
// Anthropic clients only (Claude Code / Claude Desktop wrapping their
|
|
@@ -3585,6 +3585,9 @@ async function invokeModel(body, options = {}) {
|
|
|
3585
3585
|
escalation_source: body._escalationSource ?? null,
|
|
3586
3586
|
pinned: body._pinnedRoute ? true : false,
|
|
3587
3587
|
switch_reason: body._switchReason ?? null,
|
|
3588
|
+
// TaskBand stamp for the continuation telemetry columns
|
|
3589
|
+
// (telemetry.taskbandFields reads routingResult.taskband).
|
|
3590
|
+
taskband: body._taskband ?? null,
|
|
3588
3591
|
// WS4 — off-policy evaluation from telemetry alone requires
|
|
3589
3592
|
// propensity + candidates on every row. Deterministic default is
|
|
3590
3593
|
// 1.0 with a single-entry candidate list matching the served pair.
|
|
@@ -3802,6 +3805,7 @@ async function invokeModel(body, options = {}) {
|
|
|
3802
3805
|
pinned: routingResult.pinned ? 1 : 0,
|
|
3803
3806
|
switch_reason: routingResult.switch_reason ?? null,
|
|
3804
3807
|
...telemetry.jevFields(routingResult),
|
|
3808
|
+
...telemetry.taskbandFields(routingResult),
|
|
3805
3809
|
cache_decision: routingResult._cacheDecision ?? null,
|
|
3806
3810
|
cache_read_tokens: result.json?.usage?.cache_read_input_tokens ?? null,
|
|
3807
3811
|
cache_creation_tokens: result.json?.usage?.cache_creation_input_tokens ?? null,
|
|
@@ -4038,6 +4042,7 @@ async function invokeModel(body, options = {}) {
|
|
|
4038
4042
|
pinned: routingResult.pinned ? 1 : 0,
|
|
4039
4043
|
switch_reason: routingResult.switch_reason ?? null,
|
|
4040
4044
|
...telemetry.jevFields(routingResult),
|
|
4045
|
+
...telemetry.taskbandFields(routingResult),
|
|
4041
4046
|
cache_decision: routingResult._cacheDecision ?? null,
|
|
4042
4047
|
});
|
|
4043
4048
|
|
|
@@ -4152,6 +4157,7 @@ async function invokeModel(body, options = {}) {
|
|
|
4152
4157
|
pinned: routingResult.pinned ? 1 : 0,
|
|
4153
4158
|
switch_reason: routingResult.switch_reason ?? null,
|
|
4154
4159
|
...telemetry.jevFields(routingResult),
|
|
4160
|
+
...telemetry.taskbandFields(routingResult),
|
|
4155
4161
|
cache_decision: routingResult._cacheDecision ?? null,
|
|
4156
4162
|
});
|
|
4157
4163
|
|
|
@@ -4250,6 +4256,7 @@ async function invokeModel(body, options = {}) {
|
|
|
4250
4256
|
pinned: routingResult.pinned ? 1 : 0,
|
|
4251
4257
|
switch_reason: routingResult.switch_reason ?? null,
|
|
4252
4258
|
...telemetry.jevFields(routingResult),
|
|
4259
|
+
...telemetry.taskbandFields(routingResult),
|
|
4253
4260
|
cache_decision: routingResult._cacheDecision ?? null,
|
|
4254
4261
|
cache_read_tokens: fallbackResult.json?.usage?.cache_read_input_tokens ?? null,
|
|
4255
4262
|
cache_creation_tokens: fallbackResult.json?.usage?.cache_creation_input_tokens ?? null,
|
|
@@ -4312,6 +4319,7 @@ async function invokeModel(body, options = {}) {
|
|
|
4312
4319
|
pinned: routingResult.pinned ? 1 : 0,
|
|
4313
4320
|
switch_reason: routingResult.switch_reason ?? null,
|
|
4314
4321
|
...telemetry.jevFields(routingResult),
|
|
4322
|
+
...telemetry.taskbandFields(routingResult),
|
|
4315
4323
|
cache_decision: routingResult._cacheDecision ?? null,
|
|
4316
4324
|
});
|
|
4317
4325
|
|