@konneal/engine 0.2.72 → 0.2.74

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.
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  handleAsk
3
- } from "./chunk-UW5M2AXR.js";
4
- import "./chunk-TPV5GPY2.js";
3
+ } from "./chunk-CNSTPYX3.js";
4
+ import "./chunk-7HWJOFTQ.js";
5
5
  import "./chunk-SPKPH54J.js";
6
6
  import "./chunk-A3QHHUN5.js";
7
7
  import "./chunk-LVZTWUVJ.js";
@@ -9,6 +9,7 @@ import {
9
9
  THRESHOLDS,
10
10
  answerEffort,
11
11
  effortBudget,
12
+ num,
12
13
  processExpansion,
13
14
  sha256Hex,
14
15
  today
@@ -44,13 +45,16 @@ async function rerank(ai, model, query, texts) {
44
45
  async function generateOnce(env, model, messages, effort) {
45
46
  for (let attempt2 = 0; attempt2 < 2; attempt2++) {
46
47
  try {
47
- const res = await env.AI.run(model, {
48
- messages,
49
- max_tokens: effortBudget(effort ?? answerEffort(env)),
50
- reasoning_effort: effort ?? answerEffort(env),
51
- temperature: 0.6,
52
- top_p: 0.95
53
- });
48
+ const res = await Promise.race([
49
+ env.AI.run(model, {
50
+ messages,
51
+ max_tokens: effortBudget(effort ?? answerEffort(env)),
52
+ reasoning_effort: effort ?? answerEffort(env),
53
+ temperature: 0.6,
54
+ top_p: 0.95
55
+ }),
56
+ new Promise((r) => setTimeout(() => r(null), 12e4))
57
+ ]);
54
58
  if (typeof res?.response === "string" && res.response.trim()) return res.response;
55
59
  if (typeof res?.choices?.[0]?.message?.content === "string" && res.choices[0].message.content.trim()) return res.choices[0].message.content;
56
60
  if (attempt2 === 0) console.error("generate returned empty:", model);
@@ -817,8 +821,8 @@ var BaseUrnGenerator = class {
817
821
  const ed = this.maybe("edition");
818
822
  if (ed === void 0 || ed === null)
819
823
  return void 0;
820
- const num = typeof ed === "object" && "number" in ed ? ed.number : ed;
821
- return num === void 0 || num === null || num === "" ? void 0 : `ed.${String(num)}`;
824
+ const num2 = typeof ed === "object" && "number" in ed ? ed.number : ed;
825
+ return num2 === void 0 || num2 === null || num2 === "" ? void 0 : `ed.${String(num2)}`;
822
826
  }
823
827
  urnLanguage() {
824
828
  const langs = this.maybe("languages");
@@ -7139,9 +7143,9 @@ var IsoIdentifier = class _IsoIdentifier extends BaseIdentifier {
7139
7143
  }
7140
7144
  }
7141
7145
  }
7142
- const num = this.numberPortion(true);
7143
- if (num !== "")
7144
- result += result === "" ? num : ` ${num}`;
7146
+ const num2 = this.numberPortion(true);
7147
+ if (num2 !== "")
7148
+ result += result === "" ? num2 : ` ${num2}`;
7145
7149
  result += this.languagePortion(this.languageSingle());
7146
7150
  if (this.all_parts === true)
7147
7151
  result += " (all parts)";
@@ -7306,9 +7310,9 @@ var IsoSingleIdentifier = class extends IsoIdentifier {
7306
7310
  result += `${sep}${stage}`;
7307
7311
  }
7308
7312
  }
7309
- const num = this.numberPortion(true);
7310
- if (num !== "")
7311
- result += result === "" ? num : ` ${num}`;
7313
+ const num2 = this.numberPortion(true);
7314
+ if (num2 !== "")
7315
+ result += result === "" ? num2 : ` ${num2}`;
7312
7316
  result += this.languagePortion(this.languageSingle());
7313
7317
  if (this.all_parts === true)
7314
7318
  result += " (all parts)";
@@ -7611,11 +7615,11 @@ var IsoSupplementIdentifier = class extends IsoSingleIdentifier {
7611
7615
  const ts = this.typedStageState;
7612
7616
  const stage = ts === void 0 ? "" : stageAbbr(ts.entry, long, ts.originalAbbr);
7613
7617
  let result = `${baseStr}/${stage}`;
7614
- const num = this.numberPortion(true);
7615
- if (num !== "") {
7616
- if (!num.startsWith(":") && !stage.endsWith("."))
7618
+ const num2 = this.numberPortion(true);
7619
+ if (num2 !== "") {
7620
+ if (!num2.startsWith(":") && !stage.endsWith("."))
7617
7621
  result += " ";
7618
- result += num;
7622
+ result += num2;
7619
7623
  }
7620
7624
  result += this.languagePortion(this.languageSingle());
7621
7625
  return result;
@@ -7670,20 +7674,20 @@ var IsoDirectives = class extends IsoSingleIdentifier {
7670
7674
  const abbr = this.typedStageState === void 0 ? "" : stageAbbr(this.typedStageState.entry, false);
7671
7675
  if (abbr !== "")
7672
7676
  head += ` ${abbr}`;
7673
- let num = this.number ?? "";
7677
+ let num2 = this.number ?? "";
7674
7678
  if (this.part !== void 0)
7675
- num += ` ${this.part}`;
7679
+ num2 += ` ${this.part}`;
7676
7680
  if (this.subpart !== void 0)
7677
- num += `-${this.subpart}`;
7681
+ num2 += `-${this.subpart}`;
7678
7682
  if (this.stage_iteration !== void 0)
7679
- num += `.${this.stage_iteration.render() ?? ""}`;
7683
+ num2 += `.${this.stage_iteration.render() ?? ""}`;
7680
7684
  if (this.date !== void 0 && this.date.present())
7681
- num += `:${this.date.render()}`;
7682
- if (num === "")
7685
+ num2 += `:${this.date.render()}`;
7686
+ if (num2 === "")
7683
7687
  return head;
7684
- if (num.startsWith(":"))
7685
- return head + num;
7686
- return `${head} ${num}`;
7688
+ if (num2.startsWith(":"))
7689
+ return head + num2;
7690
+ return `${head} ${num2}`;
7687
7691
  }
7688
7692
  };
7689
7693
  registerType(IsoDirectives);
@@ -8606,9 +8610,9 @@ function preprocessNist(input) {
8606
8610
  cleaned = cleaned.replace(/(?<!e)(\d)(rev\d{4})/g, "$1 $2");
8607
8611
  cleaned = cleaned.replace(/(\d+e\d+)\.([A-Za-z]{3,9}\d{4})/g, "$1rev$2");
8608
8612
  if (/\bIR\b/.test(cleaned) && !cleaned.includes("CIRC")) {
8609
- cleaned = cleaned.replace(/(\d)r(\d{1,2})\/(\d{2,4})/g, (_m, num, mon, yr) => {
8613
+ cleaned = cleaned.replace(/(\d)r(\d{1,2})\/(\d{2,4})/g, (_m, num2, mon, yr) => {
8610
8614
  const yyyy = yr.length === 2 ? `19${yr}` : yr;
8611
- return `${num}/Upd1-${yyyy}${mon.padStart(2, "0")}`;
8615
+ return `${num2}/Upd1-${yyyy}${mon.padStart(2, "0")}`;
8612
8616
  });
8613
8617
  }
8614
8618
  if (!cleaned.includes("LCIRC") && !cleaned.includes("CIRC")) {
@@ -11321,17 +11325,17 @@ function normalizeJointStageSpellings(input) {
11321
11325
  let cleaned = input;
11322
11326
  const pubs = "((?:ISO/IEC/IEEE|IEEE/ISO/IEC|IEEE/IEC/ISO|ISO/IEEE|IEC/IEEE|IEEE/IEC|ISO/IEC)(?:/ ?| ))";
11323
11327
  const stage = "(FDIS|FCD|CDV|DIS\\d?|CD\\d?|WD|PWI|NP)";
11324
- const num = "(P?\\d+(?:[.-]\\d+)?)";
11328
+ const num2 = "(P?\\d+(?:[.-]\\d+)?)";
11325
11329
  const tail = "(,? (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Sept|Oct|Nov|Dec)[a-z]* \\d{4}|$|-\\d{4}(?:-\\d\\d)?)";
11326
11330
  cleaned = cleaned.replace(/(\d)_D(\d)/g, "$1/D$2");
11327
11331
  cleaned = cleaned.replace(/,(?=[A-Za-z])/, ", ");
11328
- cleaned = cleaned.replace(new RegExp(`^${pubs}${num}/(D[\\d.]+)/ ?${stage}\\b`), "$1$4 $2/$3");
11329
- cleaned = cleaned.replace(new RegExp(`^${pubs}${num}/ ${stage}${tail}`), "$1$3 $2$4");
11330
- cleaned = cleaned.replace(new RegExp(`^${pubs}${num}/${stage} (\\d{4})\\b`), "$1$3 $2-$4");
11331
- cleaned = cleaned.replace(new RegExp(`^${pubs}${num} ${stage}${tail}`), "$1$3 $2$4");
11332
- cleaned = cleaned.replace(new RegExp(`^${pubs}${num} ${stage} (D\\d+)\\b`), "$1$3 $2/$4");
11333
- cleaned = cleaned.replace(new RegExp(`^${pubs}${num}/ ?${stage}(-(?:19|20)\\d\\d(?:-\\d\\d)?)`), "$1$3 $2$4");
11334
- cleaned = cleaned.replace(new RegExp(`^${pubs}${stage} ${num}/? ?(D\\d+)\\b`), "$1$2 $3/$4");
11332
+ cleaned = cleaned.replace(new RegExp(`^${pubs}${num2}/(D[\\d.]+)/ ?${stage}\\b`), "$1$4 $2/$3");
11333
+ cleaned = cleaned.replace(new RegExp(`^${pubs}${num2}/ ${stage}${tail}`), "$1$3 $2$4");
11334
+ cleaned = cleaned.replace(new RegExp(`^${pubs}${num2}/${stage} (\\d{4})\\b`), "$1$3 $2-$4");
11335
+ cleaned = cleaned.replace(new RegExp(`^${pubs}${num2} ${stage}${tail}`), "$1$3 $2$4");
11336
+ cleaned = cleaned.replace(new RegExp(`^${pubs}${num2} ${stage} (D\\d+)\\b`), "$1$3 $2/$4");
11337
+ cleaned = cleaned.replace(new RegExp(`^${pubs}${num2}/ ?${stage}(-(?:19|20)\\d\\d(?:-\\d\\d)?)`), "$1$3 $2$4");
11338
+ cleaned = cleaned.replace(new RegExp(`^${pubs}${stage} ${num2}/? ?(D\\d+)\\b`), "$1$2 $3/$4");
11335
11339
  return cleaned;
11336
11340
  }
11337
11341
  function preprocessIeee(input) {
@@ -14556,8 +14560,8 @@ var CenBuilder = class {
14556
14560
  const numStr = (str3(d["number"]) ?? "").trim();
14557
14561
  if (!/^\d{1,6}$/.test(numStr))
14558
14562
  return void 0;
14559
- const num = Number.parseInt(numStr, 10);
14560
- if (!(num >= 6e4 && num <= 79999))
14563
+ const num2 = Number.parseInt(numStr, 10);
14564
+ if (!(num2 >= 6e4 && num2 <= 79999))
14561
14565
  return void 0;
14562
14566
  const impl = grammarImplementation("iec");
14563
14567
  if (impl === void 0)
@@ -19006,9 +19010,9 @@ var CsaAdoptedClass = (() => {
19006
19010
  baseStr = baseStr.replace(`:${year}`, `:${year.slice(2)}`);
19007
19011
  }
19008
19012
  }
19009
- baseStr = baseStr.replace(/\/Amd\s+(\d+)([:/-])(\d{2,4})\b/g, (_m, num, sep, year) => {
19013
+ baseStr = baseStr.replace(/\/Amd\s+(\d+)([:/-])(\d{2,4})\b/g, (_m, num2, sep, year) => {
19010
19014
  const short = year.length === 4 && (year.startsWith("20") || year.startsWith("19")) ? year.slice(2) : year;
19011
- return `/A${num}${sep}${short}`;
19015
+ return `/A${num2}${sep}${short}`;
19012
19016
  });
19013
19017
  let result;
19014
19018
  if (this.publisher_prefix !== void 0 && this.publisher_prefix.endsWith("-")) {
@@ -19635,9 +19639,9 @@ function parseCsa(input) {
19635
19639
  const full = Number(two) < 50 ? `20${two}` : `19${two}`;
19636
19640
  wrapped = wrapped.replace(new RegExp(`:${two}\\b`), `:${full}`);
19637
19641
  }
19638
- wrapped = wrapped.replace(/\/A(\d+)([:/-])(\d{2,4})\b/g, (_m, num, sep, year) => {
19642
+ wrapped = wrapped.replace(/\/A(\d+)([:/-])(\d{2,4})\b/g, (_m, num2, sep, year) => {
19639
19643
  const full = year.length === 2 ? Number(year) < 50 ? `20${year}` : `19${year}` : year;
19640
- return `/Amd ${num}${sep}${full}`;
19644
+ return `/Amd ${num2}${sep}${full}`;
19641
19645
  });
19642
19646
  const base = parseExternalStandard(wrapped);
19643
19647
  if (base === void 0) {
@@ -21908,13 +21912,13 @@ function parseOimlSpine(display) {
21908
21912
  const kind = String(h._type ?? "").split(":").pop() ?? "";
21909
21913
  const letter = TYPE_LETTER[kind] ?? "";
21910
21914
  if (!letter) return null;
21911
- const num = String(Number(h.number));
21915
+ const num2 = String(Number(h.number));
21912
21916
  const part = h.part !== void 0 ? String(h.part) : void 0;
21913
21917
  const ed = h.year !== void 0 ? String(h.year) : h.edition !== void 0 ? String(h.edition) : void 0;
21914
21918
  return {
21915
- doc_number: num,
21919
+ doc_number: num2,
21916
21920
  ...ed ? { edition: ed } : {},
21917
- label: `OIML ${letter} ${num}${part ? `-${part}` : ""}${ed ? `:${ed}` : ""}`
21921
+ label: `OIML ${letter} ${num2}${part ? `-${part}` : ""}${ed ? `:${ed}` : ""}`
21918
21922
  };
21919
21923
  } catch {
21920
21924
  return null;
@@ -21949,9 +21953,9 @@ var oimlPubid = {
21949
21953
  for (const m of query.matchAll(re)) {
21950
21954
  const [, oimlPrefix, letter, gap, digits, edition] = m;
21951
21955
  if (digits.length === 1 && !oimlPrefix && !gap) continue;
21952
- const num = String(Number(digits));
21956
+ const num2 = String(Number(digits));
21953
21957
  const type = letter.toUpperCase();
21954
- return { doc_number: num, ...edition ? { edition } : {}, label: `OIML ${type} ${num}${edition ? `:${edition}` : ""}` };
21958
+ return { doc_number: num2, ...edition ? { edition } : {}, label: `OIML ${type} ${num2}${edition ? `:${edition}` : ""}` };
21955
21959
  }
21956
21960
  return null;
21957
21961
  },
@@ -24578,6 +24582,32 @@ function telemetry(env, ctx, tier, route, model, ok, answerChars, queryHash, lan
24578
24582
  ])
24579
24583
  );
24580
24584
  }
24585
+ function estimateTokens(chars) {
24586
+ return Math.ceil(chars / 4);
24587
+ }
24588
+ function tokenTier(bucketId) {
24589
+ return bucketId.startsWith("key:") ? "key" : bucketId.startsWith("sub:") ? "member" : "anon";
24590
+ }
24591
+ function tokenLimit(env, bucketId) {
24592
+ const tier = tokenTier(bucketId);
24593
+ return tier === "key" ? num(env, "KEY_DAY_TOKENS", 4e6) : tier === "member" ? num(env, "MEMBER_DAY_TOKENS", 15e5) : num(env, "ANON_DAY_TOKENS", 3e5);
24594
+ }
24595
+ async function tokenBudget(env, bucketId) {
24596
+ const used = Number(await env.CACHE.get(`t:${today()}:ask:${await sha256Hex(bucketId)}`) ?? "0");
24597
+ return { used, limit: tokenLimit(env, bucketId) };
24598
+ }
24599
+ async function chargeTokens(env, bucketId, tokens) {
24600
+ if (!(tokens > 0)) return;
24601
+ await kvIncr(env.CACHE, `t:${today()}:ask:${await sha256Hex(bucketId)}`, tokens);
24602
+ }
24603
+ function usageTotal(usage) {
24604
+ if (!usage || typeof usage !== "object") return null;
24605
+ const u = usage;
24606
+ const p = Number(u.prompt_tokens ?? 0);
24607
+ const c = Number(u.completion_tokens ?? 0);
24608
+ const total = p + c;
24609
+ return Number.isFinite(total) && total > 0 ? total : null;
24610
+ }
24581
24611
 
24582
24612
  // workers/worker_public/src/graph.ts
24583
24613
  function docNumberOf(nodeId) {
@@ -24662,6 +24692,10 @@ export {
24662
24692
  clientIp,
24663
24693
  checkQuota,
24664
24694
  telemetry,
24695
+ estimateTokens,
24696
+ tokenBudget,
24697
+ chargeTokens,
24698
+ usageTotal,
24665
24699
  graphExpand,
24666
24700
  editionNote
24667
24701
  };
@@ -3,12 +3,14 @@ import {
3
3
  answerQuality,
4
4
  appliedContext,
5
5
  buildMessages,
6
+ chargeTokens,
6
7
  checkQuota,
7
8
  citations,
8
9
  clientIp,
9
10
  contextNote,
10
11
  editionNote,
11
12
  embed,
13
+ estimateTokens,
12
14
  experimentalSourceLabels,
13
15
  generateOnce,
14
16
  graphExpand,
@@ -34,8 +36,10 @@ import {
34
36
  splitHistory,
35
37
  syntheticUnderstanding,
36
38
  telemetry,
37
- understandQuery
38
- } from "./chunk-TPV5GPY2.js";
39
+ tokenBudget,
40
+ understandQuery,
41
+ usageTotal
42
+ } from "./chunk-7HWJOFTQ.js";
39
43
  import {
40
44
  corsHeaders,
41
45
  err,
@@ -1990,6 +1994,13 @@ ${anchors.map((a) => `- ${a}`).join("\n")}`;
1990
1994
  }
1991
1995
  async function operationsCatalogNote(db, query) {
1992
1996
  if (!isOperationIntent(query)) return void 0;
1997
+ for (let attempt = 0; attempt < 2; attempt++) {
1998
+ const note = await operationsCatalogNoteOnce(db, query, attempt > 0);
1999
+ if (note !== void 0) return note;
2000
+ }
2001
+ return void 0;
2002
+ }
2003
+ async function operationsCatalogNoteOnce(db, query, retried) {
1993
2004
  try {
1994
2005
  const rows = (await db.prepare("SELECT clause_anchor AS anchor, substr(text, 1, 300) AS text FROM chunks WHERE corpus = 'smart-ops' AND clause_anchor IS NOT NULL ORDER BY clause_anchor").all()).results ?? [];
1995
2006
  const opRows = rows.map((r) => ({ anchor: String(r.anchor ?? ""), text: String(r.text ?? "") })).filter((r) => r.anchor);
@@ -2001,7 +2012,8 @@ ${matched.map((m) => `- ${m.text}`).join("\n")}`);
2001
2012
  }
2002
2013
  parts.push(catalogNote(opRows.map((r) => r.anchor)));
2003
2014
  return parts.filter(Boolean).join("\n\n") || void 0;
2004
- } catch {
2015
+ } catch (e) {
2016
+ console.error(`operations-catalog: D1 read failed${retried ? " twice" : ""} \u2014 the note is dropped`, String(e).slice(0, 120));
2005
2017
  return void 0;
2006
2018
  }
2007
2019
  }
@@ -2166,12 +2178,31 @@ async function summarizeHistory(env, model, turns) {
2166
2178
  return null;
2167
2179
  }
2168
2180
  }
2169
- async function* sseTokens(stream) {
2181
+ function messageChars(messages) {
2182
+ let n = 0;
2183
+ for (const m of messages) {
2184
+ if (typeof m.content === "string") n += m.content.length;
2185
+ else if (Array.isArray(m.content)) {
2186
+ for (const part of m.content) if (part?.type === "text" && typeof part.text === "string") n += part.text.length;
2187
+ }
2188
+ }
2189
+ return n;
2190
+ }
2191
+ async function chargeAnswerTokens(env, bucketId, stats, promptChars, answerChars) {
2192
+ const total = usageTotal(stats?.usage) ?? estimateTokens(promptChars + answerChars);
2193
+ await chargeTokens(env, bucketId, total);
2194
+ const budget = await tokenBudget(env, bucketId);
2195
+ return { tokens_used: budget.used, token_limit: budget.limit };
2196
+ }
2197
+ async function* sseTokens(stream, stats) {
2170
2198
  const reader = stream.getReader();
2171
2199
  const decoder = new TextDecoder();
2172
2200
  let buf = "";
2173
2201
  while (true) {
2174
- const { done, value } = await reader.read();
2202
+ const { done, value } = await Promise.race([
2203
+ reader.read(),
2204
+ new Promise((r) => setTimeout(() => r({ done: true }), 12e4))
2205
+ ]);
2175
2206
  if (done) break;
2176
2207
  buf += decoder.decode(value, { stream: true });
2177
2208
  const lines = buf.split("\n");
@@ -2184,6 +2215,7 @@ async function* sseTokens(stream) {
2184
2215
  try {
2185
2216
  const evt = JSON.parse(payload);
2186
2217
  const tok = typeof evt?.response === "string" ? evt.response : evt?.choices?.[0]?.delta?.content;
2218
+ if (evt.usage && stats && stats.usage === void 0) stats.usage = evt.usage;
2187
2219
  if (tok) yield tok;
2188
2220
  } catch {
2189
2221
  }
@@ -2227,6 +2259,12 @@ async function handleAsk(env, ctx, req, tier, key) {
2227
2259
  if (!quota.ok) {
2228
2260
  return err(429, "quota_exceeded", `Daily question limit reached (${quota.limit}). Try again tomorrow.`);
2229
2261
  }
2262
+ const tokBudget = await tokenBudget(env, bucketId);
2263
+ if (tokBudget.used >= tokBudget.limit) {
2264
+ return err(429, "quota_exceeded", `Daily token budget reached (${tokBudget.limit} tokens). Try again tomorrow.`);
2265
+ }
2266
+ quota.tokens_used = tokBudget.used;
2267
+ quota.token_limit = tokBudget.limit;
2230
2268
  const hardCap = num(env, "ANON_DAY_HARD_CAP", 5e3);
2231
2269
  if (tier === "anon" && quota.used > hardCap) {
2232
2270
  return err(503, "generation_disabled", "Generation is temporarily paused; search remains available.");
@@ -2444,14 +2482,17 @@ ${summary}` }] : [],
2444
2482
  `));
2445
2483
  if (readAs()) send({ type: "read", read: readAs() });
2446
2484
  send({ type: "citations", citations: [], context_applied: NO_CONTEXT, quota });
2485
+ const stats = {};
2447
2486
  let full = "";
2448
2487
  try {
2449
- for await (const tok of sseTokens(stream)) {
2488
+ for await (const tok of sseTokens(stream, stats)) {
2450
2489
  full += tok;
2451
2490
  send({ type: "token", v: tok });
2452
2491
  }
2453
2492
  } catch {
2454
2493
  }
2494
+ const tq2 = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages2), full.length);
2495
+ send({ type: "quota", quota: { ...quota, ...tq2 } });
2455
2496
  send({ type: "done", model, query_hash: queryHash2, context_applied: NO_CONTEXT, read: readAs() });
2456
2497
  telemetry(env, ctx, tier, "ask", model, true, full.length, queryHash2, q.lang, void 0, telemetryMeta());
2457
2498
  controller.close();
@@ -2507,7 +2548,8 @@ ${summary}` }] : [],
2507
2548
  corsHeaders(req)
2508
2549
  );
2509
2550
  }
2510
- return json({ answer: verdict.answer, citations: citations2, model, query_hash: queryHash2, follow_ups: [], context_applied: draftCtxApplied, read: readAs(), ...draftPayload ? { draft: draftPayload } : {}, quota });
2551
+ const tq2 = await chargeAnswerTokens(env, bucketId, void 0, q.query.length, verdict.answer.length);
2552
+ return json({ answer: verdict.answer, citations: citations2, model, query_hash: queryHash2, follow_ups: [], context_applied: draftCtxApplied, read: readAs(), ...draftPayload ? { draft: draftPayload } : {}, quota: { ...quota, ...tq2 } });
2511
2553
  }
2512
2554
  if (apiCallIntent) {
2513
2555
  const callCtxApplied = declaredCtx ? appliedContext(declaredCtx, null) : NO_CONTEXT;
@@ -2534,7 +2576,8 @@ ${summary}` }] : [],
2534
2576
  corsHeaders(req)
2535
2577
  );
2536
2578
  }
2537
- return json({ answer: verdict.answer, citations: [], model, query_hash: queryHash2, follow_ups: [], context_applied: callCtxApplied, read: readAs(), ...draftPayload ? { draft: draftPayload } : {}, quota });
2579
+ const tq2 = await chargeAnswerTokens(env, bucketId, void 0, q.query.length, verdict.answer.length);
2580
+ return json({ answer: verdict.answer, citations: [], model, query_hash: queryHash2, follow_ups: [], context_applied: callCtxApplied, read: readAs(), ...draftPayload ? { draft: draftPayload } : {}, quota: { ...quota, ...tq2 } });
2538
2581
  }
2539
2582
  let liveRecords;
2540
2583
  let accountNote;
@@ -2834,14 +2877,17 @@ ${q.query}` }],
2834
2877
  send({ type: "read", read: readAs() });
2835
2878
  const sourceQuality = answerQuality(cites.map((c) => c.quality));
2836
2879
  send({ type: "citations", citations: cites, context_applied: ctxApplied, ...sourceQuality ? { source_quality: sourceQuality, confidence_note: qualityNote(sourceQuality), ...sourceQuality === "ocr" ? { experimental_sources: experimentalSourceLabels(cites) } : {} } : {}, ...liveRecords ? { records: liveRecords } : {}, quota });
2880
+ const stats = {};
2837
2881
  let full = "";
2838
2882
  try {
2839
- for await (const tok of sseTokens(stream)) {
2883
+ for await (const tok of sseTokens(stream, stats)) {
2840
2884
  full += tok;
2841
2885
  send({ type: "token", v: tok });
2842
2886
  }
2843
2887
  } catch {
2844
2888
  }
2889
+ const tq2 = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages), full.length);
2890
+ send({ type: "quota", quota: { ...quota, ...tq2 } });
2845
2891
  const canonical0 = canonicalRefusal(full);
2846
2892
  const c2 = canonical0.includes(refusalAnswer()) ? { text: canonical0, blocks: [], dropped: [] } : await contractV2(env.DB, canonical0, usedHits);
2847
2893
  send({
@@ -3007,7 +3053,8 @@ ${q.query}` }],
3007
3053
  text: h.text.slice(0, 1200),
3008
3054
  ...h.metadata.table_selection ? { sel: h.metadata.table_selection } : {}
3009
3055
  }));
3010
- return json({ ...out, context: contextOut, read: readAs(), quota }, 200, { ...corsHeaders(req), "server-timing": serverTiming() });
3056
+ const tq = await chargeAnswerTokens(env, bucketId, void 0, messageChars(messages), answer.length);
3057
+ return json({ ...out, context: contextOut, read: readAs(), quota: { ...quota, ...tq } }, 200, { ...corsHeaders(req), "server-timing": serverTiming() });
3011
3058
  }
3012
3059
  function sseResponse(events, cors) {
3013
3060
  const encoder = new TextEncoder();
@@ -7,7 +7,7 @@ import {
7
7
  sessionFrom,
8
8
  telemetry,
9
9
  understandQuery
10
- } from "./chunk-TPV5GPY2.js";
10
+ } from "./chunk-7HWJOFTQ.js";
11
11
  import {
12
12
  corsHeaders,
13
13
  err,
package/dist/quota.d.ts CHANGED
@@ -15,3 +15,11 @@ export declare function telemetry(env: Env, ctx: Background, tier: string, route
15
15
  keyId?: string | null;
16
16
  retries?: number;
17
17
  }): void;
18
+ export declare function estimateTokens(chars: number): number;
19
+ export declare function tokenLimit(env: Env, bucketId: string): number;
20
+ export declare function tokenBudget(env: Env, bucketId: string): Promise<{
21
+ used: number;
22
+ limit: number;
23
+ }>;
24
+ export declare function chargeTokens(env: Env, bucketId: string, tokens: number): Promise<void>;
25
+ export declare function usageTotal(usage: unknown): number | null;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  handleSearch
3
- } from "./chunk-FLNX4O7C.js";
4
- import "./chunk-TPV5GPY2.js";
3
+ } from "./chunk-KO6DRI7C.js";
4
+ import "./chunk-7HWJOFTQ.js";
5
5
  import "./chunk-SPKPH54J.js";
6
6
  import "./chunk-A3QHHUN5.js";
7
7
  import "./chunk-LVZTWUVJ.js";
@@ -6,10 +6,10 @@ import {
6
6
  modelGroundingBlock,
7
7
  scoreFaithfulness,
8
8
  scoreJudge
9
- } from "../../chunk-UW5M2AXR.js";
9
+ } from "../../chunk-CNSTPYX3.js";
10
10
  import {
11
11
  handleSearch
12
- } from "../../chunk-FLNX4O7C.js";
12
+ } from "../../chunk-KO6DRI7C.js";
13
13
  import {
14
14
  buildMessages,
15
15
  cfBlobs,
@@ -35,7 +35,7 @@ import {
35
35
  sessionFrom,
36
36
  telemetry,
37
37
  understandQuery
38
- } from "../../chunk-TPV5GPY2.js";
38
+ } from "../../chunk-7HWJOFTQ.js";
39
39
  import {
40
40
  authenticate,
41
41
  corsHeaders,
@@ -959,7 +959,7 @@ async function handleMcp(env, ctx, req, tier, key) {
959
959
  // stream:false forces the JSON lane (anon defaults to SSE)
960
960
  body: JSON.stringify({ ...args, stream: false })
961
961
  });
962
- const res = name === "ask" ? await (await import("../../ask-UOTYZ5DC.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-PBOODCMQ.js")).handleSearch(env, ctx, inner, tier, key);
962
+ const res = name === "ask" ? await (await import("../../ask-FHA56VIN.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-LVUQLA3C.js")).handleSearch(env, ctx, inner, tier, key);
963
963
  return res.json().catch(() => ({ error: { message: "tool transport failed", status: res.status } }));
964
964
  });
965
965
  if (out.ok && "accepted" in out) return new Response(null, { status: 202 });
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.72",
30
+ "version": "0.2.74",
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",
@@ -54,13 +54,19 @@ export async function generateOnce(env: any, model: string, messages: any[], eff
54
54
  // silently blindfolds figure answers)
55
55
  for (let attempt = 0; attempt < 2; attempt++) {
56
56
  try {
57
- const res: any = await env.AI.run(model, {
58
- messages,
59
- max_tokens: effortBudget(effort ?? answerEffort(env)),
60
- reasoning_effort: effort ?? answerEffort(env),
61
- temperature: 0.6,
62
- top_p: 0.95,
63
- });
57
+ // bounded: a STALLED call (no error, no response — observed live
58
+ // 2026-09-29, asks hanging at zero bytes for minutes) must fall
59
+ // through to the retry/fallback, never hang the ask forever
60
+ const res: any = await Promise.race([
61
+ env.AI.run(model, {
62
+ messages,
63
+ max_tokens: effortBudget(effort ?? answerEffort(env)),
64
+ reasoning_effort: effort ?? answerEffort(env),
65
+ temperature: 0.6,
66
+ top_p: 0.95,
67
+ }),
68
+ new Promise<null>((r) => setTimeout(() => r(null), 120_000)),
69
+ ]);
64
70
  // an EMPTY string is a failed generation, not an answer: the
65
71
  // platform intermittently returns empty bodies with a 200 (observed
66
72
  // live 2026-09-20 — answers went blank platform-wide while retrieval
@@ -37,7 +37,7 @@ import { cacheKeyMaterial, corpusGen, exactCacheKey, freshRequested } from "./an
37
37
  import type { Env } from "./env";
38
38
  export type { Env };
39
39
  import { json, err, corsHeaders, readJson, validateQuery, type ApiKey } from "./lib/http";
40
- import { clientIp, checkQuota, telemetry } from "./quota";
40
+ import { chargeTokens, checkQuota, clientIp, estimateTokens, telemetry, tokenBudget, usageTotal } from "./quota";
41
41
  import { graphExpand, editionNote } from "./graph";
42
42
  import { registerNote, searchRegister, certificateLinks } from "./certificates";
43
43
  import { TOOL_DECLARATION, parseToolCall, runTool, toolNote } from "./tools";
@@ -206,12 +206,38 @@ async function summarizeHistory(
206
206
  }
207
207
  }
208
208
 
209
- async function* sseTokens(stream: ReadableStream<Uint8Array>): AsyncGenerator<string> {
209
+ /** The prompt's character weight — text parts only, image data URLs
210
+ * excluded (a photo must not read as half a million characters). */
211
+ function messageChars(messages: any[]): number {
212
+ let n = 0;
213
+ for (const m of messages) {
214
+ if (typeof m.content === "string") n += m.content.length;
215
+ else if (Array.isArray(m.content)) for (const part of m.content) if (part?.type === "text" && typeof part.text === "string") n += part.text.length;
216
+ }
217
+ return n;
218
+ }
219
+
220
+ /** Post-generation true-up: the model's own usage when the stream
221
+ * reported it, the character estimate otherwise — charged, then the
222
+ * budget's fresh state returned for the quota payload. */
223
+ async function chargeAnswerTokens(env: Env, bucketId: string, stats: { usage?: unknown } | undefined, promptChars: number, answerChars: number): Promise<{ tokens_used: number; token_limit: number }> {
224
+ const total = usageTotal(stats?.usage) ?? estimateTokens(promptChars + answerChars);
225
+ await chargeTokens(env, bucketId, total);
226
+ const budget = await tokenBudget(env, bucketId);
227
+ return { tokens_used: budget.used, token_limit: budget.limit };
228
+ }
229
+
230
+ async function* sseTokens(stream: ReadableStream<Uint8Array>, stats?: { usage?: unknown }): AsyncGenerator<string> {
210
231
  const reader = stream.getReader();
211
232
  const decoder = new TextDecoder();
212
233
  let buf = "";
213
234
  while (true) {
214
- const { done, value } = await reader.read();
235
+ // bounded read: a stalled stream (no error, no chunk) delivers what
236
+ // it has instead of hanging the ask at zero tokens
237
+ const { done, value } = await Promise.race([
238
+ reader.read(),
239
+ new Promise<{ done: true; value?: undefined }>((r) => setTimeout(() => r({ done: true }), 120_000)),
240
+ ]);
215
241
  if (done) break;
216
242
  buf += decoder.decode(value, { stream: true });
217
243
  const lines = buf.split("\n");
@@ -227,6 +253,7 @@ async function* sseTokens(stream: ReadableStream<Uint8Array>): AsyncGenerator<st
227
253
  typeof evt?.response === "string"
228
254
  ? evt.response
229
255
  : evt?.choices?.[0]?.delta?.content;
256
+ if (evt.usage && stats && stats.usage === undefined) stats.usage = evt.usage;
230
257
  if (tok) yield tok;
231
258
  } catch {
232
259
  // partial JSON in line splitting — ignore
@@ -320,6 +347,14 @@ async function handleAsk(
320
347
  if (!quota.ok) {
321
348
  return err(429, "quota_exceeded", `Daily question limit reached (${quota.limit}). Try again tomorrow.`);
322
349
  }
350
+ // the real allowance is the day's TOKENS (the 2026-09-29 shift): the
351
+ // request cap above is the anti-abuse backstop, this is the budget
352
+ const tokBudget = await tokenBudget(env, bucketId);
353
+ if (tokBudget.used >= tokBudget.limit) {
354
+ return err(429, "quota_exceeded", `Daily token budget reached (${tokBudget.limit} tokens). Try again tomorrow.`);
355
+ }
356
+ (quota as any).tokens_used = tokBudget.used;
357
+ (quota as any).token_limit = tokBudget.limit;
323
358
 
324
359
  const hardCap = num(env as any, "ANON_DAY_HARD_CAP", 5000);
325
360
  if (tier === "anon" && quota.used > hardCap) {
@@ -668,15 +703,18 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
668
703
  // the reading arrives first — a conversational turn is still a reading
669
704
  if (readAs()) send({ type: "read", read: readAs() });
670
705
  send({ type: "citations", citations: [], context_applied: NO_CONTEXT, quota, });
706
+ const stats: { usage?: unknown } = {};
671
707
  let full = "";
672
708
  try {
673
- for await (const tok of sseTokens(stream)) {
709
+ for await (const tok of sseTokens(stream, stats)) {
674
710
  full += tok;
675
711
  send({ type: "token", v: tok });
676
712
  }
677
713
  } catch {
678
714
  // stream ended prematurely — deliver what we have
679
715
  }
716
+ const tq = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages), full.length);
717
+ send({ type: "quota", quota: { ...quota, ...tq } });
680
718
  send({ type: "done", model, query_hash: queryHash, context_applied: NO_CONTEXT, read: readAs() });
681
719
  telemetry(env, ctx, tier, "ask", model, true, full.length, queryHash, q.lang, undefined, telemetryMeta());
682
720
  controller.close();
@@ -748,7 +786,8 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
748
786
  corsHeaders(req),
749
787
  );
750
788
  }
751
- return json({ answer: verdict.answer, citations, model, query_hash: queryHash, follow_ups: [], context_applied: draftCtxApplied, read: readAs(), ...(draftPayload ? { draft: draftPayload } : {}), quota, });
789
+ const tq = await chargeAnswerTokens(env, bucketId, undefined, q.query.length, verdict.answer.length);
790
+ return json({ answer: verdict.answer, citations, model, query_hash: queryHash, follow_ups: [], context_applied: draftCtxApplied, read: readAs(), ...(draftPayload ? { draft: draftPayload } : {}), quota: { ...quota, ...tq } });
752
791
  }
753
792
 
754
793
  // ── The api_call draft (TODO.ai-platform/09) — the operations
@@ -785,7 +824,8 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
785
824
  corsHeaders(req),
786
825
  );
787
826
  }
788
- return json({ answer: verdict.answer, citations: [], model, query_hash: queryHash, follow_ups: [], context_applied: callCtxApplied, read: readAs(), ...(draftPayload ? { draft: draftPayload } : {}), quota, });
827
+ const tq = await chargeAnswerTokens(env, bucketId, undefined, q.query.length, verdict.answer.length);
828
+ return json({ answer: verdict.answer, citations: [], model, query_hash: queryHash, follow_ups: [], context_applied: callCtxApplied, read: readAs(), ...(draftPayload ? { draft: draftPayload } : {}), quota: { ...quota, ...tq } });
789
829
  }
790
830
 
791
831
  // (declared before the retrieval try: the account block, the refusal
@@ -1227,15 +1267,18 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1227
1267
  send({ type: "read", read: readAs() });
1228
1268
  const sourceQuality = answerQuality(cites.map((c: any) => c.quality));
1229
1269
  send({ type: "citations", citations: cites, context_applied: ctxApplied, ...(sourceQuality ? { source_quality: sourceQuality, confidence_note: qualityNote(sourceQuality), ...(sourceQuality === "ocr" ? { experimental_sources: experimentalSourceLabels(cites) } : {}) } : {}), ...(liveRecords ? { records: liveRecords } : {}), quota, });
1270
+ const stats: { usage?: unknown } = {};
1230
1271
  let full = "";
1231
1272
  try {
1232
- for await (const tok of sseTokens(stream)) {
1273
+ for await (const tok of sseTokens(stream, stats)) {
1233
1274
  full += tok;
1234
1275
  send({ type: "token", v: tok });
1235
1276
  }
1236
1277
  } catch {
1237
1278
  // stream ended prematurely — deliver what we have
1238
1279
  }
1280
+ const tq = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages), full.length);
1281
+ send({ type: "quota", quota: { ...quota, ...tq } });
1239
1282
  const canonical0 = canonicalRefusal(full);
1240
1283
  // answer contract v2: validate [[u:]] refs, resolve typed blocks
1241
1284
  const c2 = canonical0.includes(refusalAnswer())
@@ -1475,7 +1518,8 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1475
1518
  text: h.text.slice(0, 1200),
1476
1519
  ...((h.metadata as any).table_selection ? { sel: (h.metadata as any).table_selection } : {}),
1477
1520
  }));
1478
- return json({ ...out, context: contextOut, read: readAs(), quota }, 200, { ...corsHeaders(req), "server-timing": serverTiming() });
1521
+ const tq = await chargeAnswerTokens(env, bucketId, undefined, messageChars(messages), answer.length);
1522
+ return json({ ...out, context: contextOut, read: readAs(), quota: { ...quota, ...tq } }, 200, { ...corsHeaders(req), "server-timing": serverTiming() });
1479
1523
  }
1480
1524
 
1481
1525
  function sseResponse(events: unknown[], cors: Record<string, string>): Response {
@@ -70,7 +70,18 @@ export function catalogNote(anchors: string[]): string {
70
70
 
71
71
  export async function operationsCatalogNote(db: any, query: string): Promise<string | undefined> {
72
72
  if (!isOperationIntent(query)) return undefined;
73
- try {
73
+ // one retry, and the failure is LOUD: a silently dropped note answers
74
+ // an operation question with the OIML refusal (observed live 2026-09-29
75
+ // — a transient D1 read failed into the catch and the lane vanished)
76
+ for (let attempt = 0; attempt < 2; attempt++) {
77
+ const note = await operationsCatalogNoteOnce(db, query, attempt > 0);
78
+ if (note !== undefined) return note;
79
+ }
80
+ return undefined;
81
+ }
82
+
83
+ async function operationsCatalogNoteOnce(db: any, query: string, retried: boolean): Promise<string | undefined> {
84
+ try {
74
85
  const rows = (await db.prepare("SELECT clause_anchor AS anchor, substr(text, 1, 300) AS text FROM chunks WHERE corpus = 'smart-ops' AND clause_anchor IS NOT NULL ORDER BY clause_anchor").all()).results ?? [];
75
86
  const opRows = (rows as any[]).map((r) => ({ anchor: String(r.anchor ?? ""), text: String(r.text ?? "") })).filter((r) => r.anchor);
76
87
  const matched = matchOperations(query, opRows);
@@ -80,7 +91,9 @@ export async function operationsCatalogNote(db: any, query: string): Promise<str
80
91
  }
81
92
  parts.push(catalogNote(opRows.map((r) => r.anchor)));
82
93
  return parts.filter(Boolean).join("\n\n") || undefined;
83
- } catch {
84
- return undefined; // the lane is optional
94
+ } catch (e) {
95
+ // the lane is optional — but never silent (the blind-failure lesson)
96
+ console.error(`operations-catalog: D1 read failed${retried ? " twice" : ""} — the note is dropped`, String(e).slice(0, 120));
97
+ return undefined;
85
98
  }
86
99
  }
@@ -1,6 +1,6 @@
1
1
  // Quota + telemetry: per-bucket daily counters (KV) and the D1
2
2
  // queries/spend ledger writes (TODO.impl/23).
3
- import { sha256Hex, today } from "./config";
3
+ import { num, sha256Hex, today } from "./config.ts";
4
4
  import type { Env } from "./env";
5
5
  import type { Background } from "./ports/runtime.ts";
6
6
 
@@ -54,3 +54,44 @@ export function telemetry(
54
54
  ]),
55
55
  );
56
56
  }
57
+
58
+ // ── the token budget (2026-09-29): the daily allowance is TOKENS, not
59
+ // questions — an elevated-effort answer over a wide retrieval context
60
+ // costs orders of magnitude more than a cached refusal, and one number
61
+ // should say so. Charged after generation from the model's own usage
62
+ // when the stream reports it, estimated by characters otherwise.
63
+ export function estimateTokens(chars: number): number {
64
+ return Math.ceil(chars / 4);
65
+ }
66
+
67
+ function tokenTier(bucketId: string): "anon" | "member" | "key" {
68
+ return bucketId.startsWith("key:") ? "key" : bucketId.startsWith("sub:") ? "member" : "anon";
69
+ }
70
+
71
+ export function tokenLimit(env: Env, bucketId: string): number {
72
+ const tier = tokenTier(bucketId);
73
+ return tier === "key"
74
+ ? num(env as any, "KEY_DAY_TOKENS", 4_000_000)
75
+ : tier === "member"
76
+ ? num(env as any, "MEMBER_DAY_TOKENS", 1_500_000)
77
+ : num(env as any, "ANON_DAY_TOKENS", 300_000);
78
+ }
79
+
80
+ export async function tokenBudget(env: Env, bucketId: string): Promise<{ used: number; limit: number }> {
81
+ const used = Number((await env.CACHE.get(`t:${today()}:ask:${await sha256Hex(bucketId)}`)) ?? "0");
82
+ return { used, limit: tokenLimit(env, bucketId) };
83
+ }
84
+
85
+ export async function chargeTokens(env: Env, bucketId: string, tokens: number): Promise<void> {
86
+ if (!(tokens > 0)) return;
87
+ await kvIncr(env.CACHE, `t:${today()}:ask:${await sha256Hex(bucketId)}`, tokens);
88
+ }
89
+
90
+ export function usageTotal(usage: unknown): number | null {
91
+ if (!usage || typeof usage !== "object") return null;
92
+ const u = usage as any;
93
+ const p = Number(u.prompt_tokens ?? 0);
94
+ const c = Number(u.completion_tokens ?? 0);
95
+ const total = p + c;
96
+ return Number.isFinite(total) && total > 0 ? total : null;
97
+ }