@konneal/engine 0.2.134 → 0.2.136
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/dist/{ask-PFU74MIP.js → ask-N5I35VCK.js} +2 -2
- package/dist/{chunk-WPOKSNPE.js → chunk-XBZIJMS7.js} +6 -1
- package/dist/{chunk-JYJJ4SYF.js → chunk-YLBPABDQ.js} +10 -8
- package/dist/refusal.d.ts +8 -0
- package/dist/{search-NIRCPRMW.js → search-LFS5HHMZ.js} +1 -1
- package/dist/worker_public/src/index.js +4 -4
- package/dist/worker_public/src/refusal.js +3 -1
- package/package.json +1 -1
- package/workers/worker_public/src/ask.ts +14 -9
- package/workers/worker_public/src/refusal.ts +12 -0
- package/workers/worker_public/src/research.ts +1 -1
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
handleAsk
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-YLBPABDQ.js";
|
|
4
4
|
import "./chunk-J6LBR6GI.js";
|
|
5
5
|
import "./chunk-7AXXR7GD.js";
|
|
6
6
|
import "./chunk-TH5XGPD5.js";
|
|
7
7
|
import "./chunk-W7J5SFXJ.js";
|
|
8
|
-
import "./chunk-
|
|
8
|
+
import "./chunk-XBZIJMS7.js";
|
|
9
9
|
import "./chunk-4NXH5ZI6.js";
|
|
10
10
|
import "./chunk-VT7DR6NQ.js";
|
|
11
11
|
import "./chunk-GBGRHXRU.js";
|
|
@@ -97,9 +97,14 @@ function exciseMisplacedPin(answer, cites) {
|
|
|
97
97
|
}
|
|
98
98
|
return out.replace(/^[ \t\n]+/, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
99
99
|
}
|
|
100
|
+
function cacheableAnswer(answer, scope) {
|
|
101
|
+
if (answer.includes(refusalAnswer())) return false;
|
|
102
|
+
return canonicalRefusal(answer, scope) === answer;
|
|
103
|
+
}
|
|
100
104
|
|
|
101
105
|
export {
|
|
102
106
|
refusalAnswer,
|
|
103
107
|
canonicalRefusal,
|
|
104
|
-
exciseMisplacedPin
|
|
108
|
+
exciseMisplacedPin,
|
|
109
|
+
cacheableAnswer
|
|
105
110
|
};
|
|
@@ -80,10 +80,11 @@ import {
|
|
|
80
80
|
validateQuery
|
|
81
81
|
} from "./chunk-W7J5SFXJ.js";
|
|
82
82
|
import {
|
|
83
|
+
cacheableAnswer,
|
|
83
84
|
canonicalRefusal,
|
|
84
85
|
exciseMisplacedPin,
|
|
85
86
|
refusalAnswer
|
|
86
|
-
} from "./chunk-
|
|
87
|
+
} from "./chunk-XBZIJMS7.js";
|
|
87
88
|
import {
|
|
88
89
|
entitlementScope,
|
|
89
90
|
requestSalt,
|
|
@@ -1980,6 +1981,7 @@ async function handleAsk(env, ctx, req, tier, key) {
|
|
|
1980
1981
|
}
|
|
1981
1982
|
}
|
|
1982
1983
|
let understanding = null;
|
|
1984
|
+
const answerCites = (t) => /\[[^\]]+\]/.test(t);
|
|
1983
1985
|
const refusalScope = (cited) => ({ cited, conversational: understanding?.intent === "conversational" });
|
|
1984
1986
|
let optimisticVec = null;
|
|
1985
1987
|
let optimisticHits = [];
|
|
@@ -2621,7 +2623,7 @@ ${q.query}`.replace("\n\n\n\n", "\n\n") }];
|
|
|
2621
2623
|
}
|
|
2622
2624
|
}
|
|
2623
2625
|
}
|
|
2624
|
-
const canonical0 = usedHits.length > 0 ? exciseMisplacedPin(canonicalRefusal(full, refusalScope(
|
|
2626
|
+
const canonical0 = usedHits.length > 0 ? exciseMisplacedPin(canonicalRefusal(full, refusalScope(answerCites(full))), true) : canonicalRefusal(full, refusalScope(false));
|
|
2625
2627
|
const c2 = canonical0.includes(refusalAnswer()) ? { text: canonical0, blocks: [], dropped: [] } : await contractV2(env.DB, canonical0, usedHits);
|
|
2626
2628
|
send({
|
|
2627
2629
|
type: "done",
|
|
@@ -2659,7 +2661,7 @@ ${q.query}`.replace("\n\n\n\n", "\n\n") }];
|
|
|
2659
2661
|
if (streamed.violations.length > 0) {
|
|
2660
2662
|
console.log("anchors:", streamed.violations.length, "of", streamed.total, "unverified \u2014 not caching");
|
|
2661
2663
|
}
|
|
2662
|
-
if (streamed.violations.length === 0 && canonical.length > 0 && !contextual && !declaredCtx &&
|
|
2664
|
+
if (streamed.violations.length === 0 && canonical.length > 0 && !contextual && !declaredCtx && cacheableAnswer(canonical, refusalScope(answerCites(canonical))) && !ablate) {
|
|
2663
2665
|
ctx.waitUntil(
|
|
2664
2666
|
env.CACHE.put(exactCacheKey(env.INDEX_VERSION, gen, ns, await sha256Hex(cacheKeyMaterial(q.query, q.lang, salt))), JSON.stringify({ answer: canonical, citations: cites, model, query_hash: queryHash, ...sourceQuality ? { source_quality: sourceQuality, confidence_note: streamedNote || qualityNote(sourceQuality) } : {}, ...streamedEntailment ? { entailment: streamedEntailment } : {} }), { expirationTtl: LIMITS.cacheTtlSec })
|
|
2665
2667
|
);
|
|
@@ -2705,7 +2707,7 @@ ${q.query}`.replace("\n\n\n\n", "\n\n") }];
|
|
|
2705
2707
|
);
|
|
2706
2708
|
answer = await generateOnce(env, MODELS.fallback, flat, routeEffort(routed.route));
|
|
2707
2709
|
}
|
|
2708
|
-
if (answer) answer = canonicalRefusal(answer, refusalScope(
|
|
2710
|
+
if (answer) answer = canonicalRefusal(answer, refusalScope(answerCites(answer)));
|
|
2709
2711
|
if (answer && usedHits.length > 0) answer = exciseMisplacedPin(answer, true);
|
|
2710
2712
|
stageTiming.generate = Date.now() - tGen;
|
|
2711
2713
|
let used = usedHits;
|
|
@@ -2730,7 +2732,7 @@ ${q.query}`.replace("\n\n\n\n", "\n\n") }];
|
|
|
2730
2732
|
generateRetries += 1;
|
|
2731
2733
|
const corrected = await generateOnce(env, model, [...messages, { role: "system", content: note }], routeEffort(routed.route));
|
|
2732
2734
|
if (corrected) {
|
|
2733
|
-
const correctedAnswer = canonicalRefusal(corrected, refusalScope(
|
|
2735
|
+
const correctedAnswer = canonicalRefusal(corrected, refusalScope(answerCites(corrected)));
|
|
2734
2736
|
const retryAnchors = checkQuoteAnchors(correctedAnswer, used.map((h) => h.text));
|
|
2735
2737
|
const retryRetyped = tableRetyped(correctedAnswer, hasTableUnit);
|
|
2736
2738
|
if (retryAnchors.violations.length < anchors.violations.length || !retryRetyped && retyped || unreferenced && correctedAnswer.includes("[[u:")) {
|
|
@@ -2755,7 +2757,7 @@ ${q.query}`.replace("\n\n\n\n", "\n\n") }];
|
|
|
2755
2757
|
const { messages: retryMessages, usedHits: retryUsed } = buildMessages(q.query, retryRetrieve.hits, q.lang, keptHistory, void 0, summary, budget);
|
|
2756
2758
|
const retryAnswer = await generateOnce(env, model, retryMessages, routeEffort(routed.route));
|
|
2757
2759
|
if (retryAnswer) {
|
|
2758
|
-
answer = canonicalRefusal(retryAnswer, refusalScope(
|
|
2760
|
+
answer = canonicalRefusal(retryAnswer, refusalScope(answerCites(retryAnswer)));
|
|
2759
2761
|
used = retryUsed;
|
|
2760
2762
|
}
|
|
2761
2763
|
}
|
|
@@ -2787,7 +2789,7 @@ ${q.query}`.replace("\n\n\n\n", "\n\n") }];
|
|
|
2787
2789
|
messages[messages.length - 1]
|
|
2788
2790
|
], effort);
|
|
2789
2791
|
if (regen && !regen.includes(refusalAnswer())) {
|
|
2790
|
-
answer = canonicalRefusal(regen, refusalScope(
|
|
2792
|
+
answer = canonicalRefusal(regen, refusalScope(answerCites(regen)));
|
|
2791
2793
|
console.log("blind-answer: regeneration grounded");
|
|
2792
2794
|
}
|
|
2793
2795
|
}
|
|
@@ -2807,7 +2809,7 @@ ${q.query}`.replace("\n\n\n\n", "\n\n") }];
|
|
|
2807
2809
|
}
|
|
2808
2810
|
}
|
|
2809
2811
|
const out = { answer, citations: finalCites, ...jsonQuality ? { source_quality: jsonQuality, confidence_note: entailmentNote || qualityNote(jsonQuality), ...entailment ? { entailment } : {}, ...jsonQuality === "ocr" ? { experimental_sources: experimentalSourceLabels(finalCites) } : {} } : {}, model: MODELS.member, query_hash: queryHash, follow_ups: understanding?.follow_ups ?? [], blocks: [...c2ns.blocks, ...verdictBlock ? [verdictBlock] : [], ...conditionBlock ? [conditionBlock] : [], ...aggregationBlock ? [aggregationBlock] : [], ...registerBlock ? [registerBlock] : [], ...completionBlocks], context_applied: ctxApplied, ...liveRecords ? { records: liveRecords } : {}, ...ablate ? { ablate } : {}, route: { route: routed.route, features: routed.features } };
|
|
2810
|
-
const cacheable = !contextual && !declaredCtx && !ablate &&
|
|
2812
|
+
const cacheable = !contextual && !declaredCtx && !ablate && cacheableAnswer(answer, refusalScope(answerCites(answer))) && finalAnchors.violations.length === 0;
|
|
2811
2813
|
if (cacheable) {
|
|
2812
2814
|
const ck = exactCacheKey(env.INDEX_VERSION, gen, ns, await sha256Hex(cacheKeyMaterial(q.query, q.lang, salt)));
|
|
2813
2815
|
ctx.waitUntil(env.CACHE.put(ck, JSON.stringify(out), { expirationTtl: LIMITS.cacheTtlSec }));
|
package/dist/refusal.d.ts
CHANGED
|
@@ -32,3 +32,11 @@ export declare function canonicalRefusal(answer: string, scope?: RefusalScope):
|
|
|
32
32
|
* the pin sentence (and its refusal-shaped paraphrases) are excised —
|
|
33
33
|
* the surrounding content stands on its own. */
|
|
34
34
|
export declare function exciseMisplacedPin(answer: string, cites: boolean): string;
|
|
35
|
+
/** Cacheable under the refusal law (2026-10-11, the poisoned-entry
|
|
36
|
+
* lesson): "refusals are never cached" was enforced as "answers
|
|
37
|
+
* containing the pin are never cached" — backwards for DRIFT, which
|
|
38
|
+
* lacks the pin and cached itself across deploy windows (an edge
|
|
39
|
+
* instance running pre-wall code wrote a drifted refusal that served
|
|
40
|
+
* every later ask). An answer is cacheable only when it carries no
|
|
41
|
+
* pin AND the wall would not rewrite it. */
|
|
42
|
+
export declare function cacheableAnswer(answer: string, scope?: RefusalScope): boolean;
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
handleMemories,
|
|
5
5
|
scoreFaithfulness,
|
|
6
6
|
scoreJudge
|
|
7
|
-
} from "../../chunk-
|
|
7
|
+
} from "../../chunk-YLBPABDQ.js";
|
|
8
8
|
import {
|
|
9
9
|
TOOLS_REGISTRY,
|
|
10
10
|
bindModelNode,
|
|
@@ -54,7 +54,7 @@ import {
|
|
|
54
54
|
} from "../../chunk-W7J5SFXJ.js";
|
|
55
55
|
import {
|
|
56
56
|
canonicalRefusal
|
|
57
|
-
} from "../../chunk-
|
|
57
|
+
} from "../../chunk-XBZIJMS7.js";
|
|
58
58
|
import {
|
|
59
59
|
entitlementScope,
|
|
60
60
|
standardKeysFrom
|
|
@@ -877,7 +877,7 @@ ${recent.map((h, n) => `[${n + 1}] ${h.metadata.docidentifier ?? ""} \xA7${h.met
|
|
|
877
877
|
telemetry(env, ctx, "member", "research", MODELS.research, false, 0, queryHash, q.lang);
|
|
878
878
|
throw new Error("The generation model is unavailable; please retry.");
|
|
879
879
|
}
|
|
880
|
-
answer = canonicalRefusal(answer, { cited:
|
|
880
|
+
answer = canonicalRefusal(answer, { cited: /\[[^\]]+\]/.test(answer), conversational: false });
|
|
881
881
|
const anchors = checkQuoteAnchors(answer, used.map((h) => h.text));
|
|
882
882
|
if (anchors.violations.length) console.log("research anchors:", anchors.violations.length, "unverified");
|
|
883
883
|
const out = {
|
|
@@ -1052,7 +1052,7 @@ async function callToolOnce(env, ctx, _req, tier, key, name, args, t0) {
|
|
|
1052
1052
|
const r = await runTool({ ...env, DB: env.DB }, { name, args }, "mcp");
|
|
1053
1053
|
payload = r ? { tool: r.name, query: r.query, result: r.output } : { error: { message: `tool ${name} returned nothing for the given arguments` } };
|
|
1054
1054
|
} else {
|
|
1055
|
-
const res = name === "ask" ? await (await import("../../ask-
|
|
1055
|
+
const res = name === "ask" ? await (await import("../../ask-N5I35VCK.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-LFS5HHMZ.js")).handleSearch(env, ctx, inner, tier, key);
|
|
1056
1056
|
payload = await res.json().catch(() => ({ error: { message: "tool transport failed", status: res.status } }));
|
|
1057
1057
|
}
|
|
1058
1058
|
telemetry(env, ctx, tier, `mcp:${name}`, null, !payload?.error, JSON.stringify(payload).length, await sha256Hex(`${name}:${JSON.stringify(args ?? {})}`), void 0, void 0, { durationMs: Date.now() - t0 });
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
+
cacheableAnswer,
|
|
2
3
|
canonicalRefusal,
|
|
3
4
|
exciseMisplacedPin,
|
|
4
5
|
refusalAnswer
|
|
5
|
-
} from "../../chunk-
|
|
6
|
+
} from "../../chunk-XBZIJMS7.js";
|
|
6
7
|
import "../../chunk-3FYJM7LH.js";
|
|
7
8
|
export {
|
|
9
|
+
cacheableAnswer,
|
|
8
10
|
canonicalRefusal,
|
|
9
11
|
exciseMisplacedPin,
|
|
10
12
|
refusalAnswer
|
package/package.json
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"playwright": "^1.62.1",
|
|
28
28
|
"yaml": "^2.9.1"
|
|
29
29
|
},
|
|
30
|
-
"version": "0.2.
|
|
30
|
+
"version": "0.2.136",
|
|
31
31
|
"description": "The Konneal engine: the publisher-agnostic build pipeline and API plane for standards intelligence (retrieval, answer contract, verdicts, evaluation).",
|
|
32
32
|
"license": "BSD-3-Clause",
|
|
33
33
|
"type": "module",
|
|
@@ -17,7 +17,7 @@ import { reflect } from "./reflect";
|
|
|
17
17
|
import { scoreFaithfulness } from "./faithfulness";
|
|
18
18
|
import { entailmentVerdict } from "./entailment";
|
|
19
19
|
import { checkQuoteAnchors, ANCHOR_CORRECTION_NOTE } from "./anchors";
|
|
20
|
-
import { canonicalRefusal, exciseMisplacedPin } from "./refusal";
|
|
20
|
+
import { cacheableAnswer, canonicalRefusal, exciseMisplacedPin } from "./refusal";
|
|
21
21
|
import { contractV2, tableRetyped } from "./refs";
|
|
22
22
|
import { completeTables, completeFigures } from "./completion";
|
|
23
23
|
import { NO_CONTEXT, appliedContext, contextNote, namedDocumentIn, parseContext, resolveDocScope, syntheticUnderstanding } from "./context";
|
|
@@ -627,7 +627,12 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
627
627
|
let understanding: any = null;
|
|
628
628
|
// the loose redirect-offer family's gate (refusal.ts): knowledge-seek
|
|
629
629
|
// questions only, uncited answers only — a conversational turn's
|
|
630
|
-
// scope vocabulary is the answer itself
|
|
630
|
+
// scope vocabulary is the answer itself. "Cited" is the ANSWER TEXT
|
|
631
|
+
// carrying citation markers, NEVER the prompt's hit count: retrieval
|
|
632
|
+
// always returns passages (the model just declines to cite them on a
|
|
633
|
+
// refusal) — gating on usedHits left every refusal "cited" and the
|
|
634
|
+
// wall dark through three deploys (2026-10-11)
|
|
635
|
+
const answerCites = (t: string) => /\[[^\]]+\]/.test(t);
|
|
631
636
|
const refusalScope = (cited: boolean) => ({ cited, conversational: understanding?.intent === "conversational" });
|
|
632
637
|
// a query naming a model node (/req/…, /term/…) is node-SCOPED: its
|
|
633
638
|
// ── Option C: optimistic parallel retrieval ──
|
|
@@ -1541,7 +1546,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1541
1546
|
}
|
|
1542
1547
|
// the misplaced-pin wall rides the streamed path too: a
|
|
1543
1548
|
// CITED stream is not a refusal
|
|
1544
|
-
const canonical0 = usedHits.length > 0 ? exciseMisplacedPin(canonicalRefusal(full, refusalScope(
|
|
1549
|
+
const canonical0 = usedHits.length > 0 ? exciseMisplacedPin(canonicalRefusal(full, refusalScope(answerCites(full))), true) : canonicalRefusal(full, refusalScope(false));
|
|
1545
1550
|
// answer contract v2: validate [[u:]] refs, resolve typed blocks
|
|
1546
1551
|
const c2 = canonical0.includes(refusalAnswer())
|
|
1547
1552
|
? { text: canonical0, blocks: [], dropped: [] as string[] }
|
|
@@ -1582,7 +1587,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1582
1587
|
if (streamed.violations.length > 0) {
|
|
1583
1588
|
console.log("anchors:", streamed.violations.length, "of", streamed.total, "unverified — not caching");
|
|
1584
1589
|
}
|
|
1585
|
-
if (streamed.violations.length === 0 && canonical.length > 0 && !contextual && !declaredCtx &&
|
|
1590
|
+
if (streamed.violations.length === 0 && canonical.length > 0 && !contextual && !declaredCtx && cacheableAnswer(canonical, refusalScope(answerCites(canonical))) && !ablate) {
|
|
1586
1591
|
ctx.waitUntil(
|
|
1587
1592
|
env.CACHE.put(exactCacheKey(env.INDEX_VERSION, gen, ns, await sha256Hex(cacheKeyMaterial(q.query, q.lang, salt))), JSON.stringify({ answer: canonical, citations: cites, model, query_hash: queryHash, ...(sourceQuality ? { source_quality: sourceQuality, confidence_note: streamedNote || qualityNote(sourceQuality) } : {}), ...(streamedEntailment ? { entailment: streamedEntailment } : {}) }), { expirationTtl: LIMITS.cacheTtlSec }),
|
|
1588
1593
|
);
|
|
@@ -1645,7 +1650,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1645
1650
|
);
|
|
1646
1651
|
answer = await generateOnce(env, MODELS.fallback, flat, routeEffort(routed.route));
|
|
1647
1652
|
}
|
|
1648
|
-
if (answer) answer = canonicalRefusal(answer, refusalScope(
|
|
1653
|
+
if (answer) answer = canonicalRefusal(answer, refusalScope(answerCites(answer)));
|
|
1649
1654
|
// the misplaced-pin wall: a CITED answer is not a refusal — excise
|
|
1650
1655
|
// the pinned sentence if the model dropped it in as a disclaimer
|
|
1651
1656
|
// (measured on chip-scoped asks: correct cited answers carrying the
|
|
@@ -1688,7 +1693,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1688
1693
|
generateRetries += 1;
|
|
1689
1694
|
const corrected = await generateOnce(env, model, [...messages, { role: "system", content: note }], routeEffort(routed.route));
|
|
1690
1695
|
if (corrected) {
|
|
1691
|
-
const correctedAnswer = canonicalRefusal(corrected, refusalScope(
|
|
1696
|
+
const correctedAnswer = canonicalRefusal(corrected, refusalScope(answerCites(corrected)));
|
|
1692
1697
|
const retryAnchors = checkQuoteAnchors(correctedAnswer, used.map((h: Hit) => h.text));
|
|
1693
1698
|
const retryRetyped = tableRetyped(correctedAnswer, hasTableUnit);
|
|
1694
1699
|
if (retryAnchors.violations.length < anchors.violations.length || (!retryRetyped && retyped) || (unreferenced && correctedAnswer.includes("[[u:"))) {
|
|
@@ -1733,7 +1738,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1733
1738
|
const retryAnswer = await generateOnce(env, model, retryMessages, routeEffort(routed.route));
|
|
1734
1739
|
// the answer now comes from the retry passages — citations must follow
|
|
1735
1740
|
if (retryAnswer) {
|
|
1736
|
-
answer = canonicalRefusal(retryAnswer, refusalScope(
|
|
1741
|
+
answer = canonicalRefusal(retryAnswer, refusalScope(answerCites(retryAnswer)));
|
|
1737
1742
|
used = retryUsed;
|
|
1738
1743
|
}
|
|
1739
1744
|
}
|
|
@@ -1785,7 +1790,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1785
1790
|
messages[messages.length - 1],
|
|
1786
1791
|
], effort);
|
|
1787
1792
|
if (regen && !regen.includes(refusalAnswer())) {
|
|
1788
|
-
answer = canonicalRefusal(regen, refusalScope(
|
|
1793
|
+
answer = canonicalRefusal(regen, refusalScope(answerCites(regen)));
|
|
1789
1794
|
console.log("blind-answer: regeneration grounded");
|
|
1790
1795
|
}
|
|
1791
1796
|
}
|
|
@@ -1811,7 +1816,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1811
1816
|
}
|
|
1812
1817
|
}
|
|
1813
1818
|
const out = { answer, citations: finalCites, ...(jsonQuality ? { source_quality: jsonQuality, confidence_note: entailmentNote || qualityNote(jsonQuality), ...(entailment ? { entailment } : {}), ...(jsonQuality === "ocr" ? { experimental_sources: experimentalSourceLabels(finalCites) } : {}) } : {}), model: MODELS.member, query_hash: queryHash, follow_ups: understanding?.follow_ups ?? [], blocks: [...c2ns.blocks, ...(verdictBlock ? [verdictBlock] : []), ...(conditionBlock ? [conditionBlock] : []), ...(aggregationBlock ? [aggregationBlock] : []), ...(registerBlock ? [registerBlock] : []), ...completionBlocks], context_applied: ctxApplied, ...(liveRecords ? { records: liveRecords } : {}), ...(ablate ? { ablate } : {}), route: { route: routed.route, features: routed.features } };
|
|
1814
|
-
const cacheable = !contextual && !declaredCtx && !ablate &&
|
|
1819
|
+
const cacheable = !contextual && !declaredCtx && !ablate && cacheableAnswer(answer, refusalScope(answerCites(answer))) && finalAnchors.violations.length === 0;
|
|
1815
1820
|
if (cacheable) {
|
|
1816
1821
|
const ck = exactCacheKey(env.INDEX_VERSION, gen, ns, await sha256Hex(cacheKeyMaterial(q.query, q.lang, salt)));
|
|
1817
1822
|
ctx.waitUntil(env.CACHE.put(ck, JSON.stringify(out), { expirationTtl: LIMITS.cacheTtlSec }));
|
|
@@ -146,3 +146,15 @@ export function exciseMisplacedPin(answer: string, cites: boolean): string {
|
|
|
146
146
|
}
|
|
147
147
|
return out.replace(/^[ \t\n]+/, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
148
148
|
}
|
|
149
|
+
|
|
150
|
+
/** Cacheable under the refusal law (2026-10-11, the poisoned-entry
|
|
151
|
+
* lesson): "refusals are never cached" was enforced as "answers
|
|
152
|
+
* containing the pin are never cached" — backwards for DRIFT, which
|
|
153
|
+
* lacks the pin and cached itself across deploy windows (an edge
|
|
154
|
+
* instance running pre-wall code wrote a drifted refusal that served
|
|
155
|
+
* every later ask). An answer is cacheable only when it carries no
|
|
156
|
+
* pin AND the wall would not rewrite it. */
|
|
157
|
+
export function cacheableAnswer(answer: string, scope?: RefusalScope): boolean {
|
|
158
|
+
if (answer.includes(refusalAnswer())) return false;
|
|
159
|
+
return canonicalRefusal(answer, scope) === answer;
|
|
160
|
+
}
|
|
@@ -164,7 +164,7 @@ async function run(
|
|
|
164
164
|
telemetry(env, ctx, "member", "research", MODELS.research, false, 0, queryHash, q.lang);
|
|
165
165
|
throw new Error("The generation model is unavailable; please retry.");
|
|
166
166
|
}
|
|
167
|
-
answer = canonicalRefusal(answer, { cited:
|
|
167
|
+
answer = canonicalRefusal(answer, { cited: /\[[^\]]+\]/.test(answer), conversational: false });
|
|
168
168
|
const anchors = checkQuoteAnchors(answer, used.map((h: Hit) => h.text));
|
|
169
169
|
if (anchors.violations.length) console.log("research anchors:", anchors.violations.length, "unverified");
|
|
170
170
|
const out = {
|