@konneal/engine 0.2.71 → 0.2.73

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,12 +1,12 @@
1
1
  import {
2
2
  handleAsk
3
- } from "./chunk-EOQVQBS6.js";
4
- import "./chunk-TPV5GPY2.js";
3
+ } from "./chunk-V7PEVNNO.js";
4
+ import "./chunk-LL3LWBZH.js";
5
5
  import "./chunk-SPKPH54J.js";
6
6
  import "./chunk-A3QHHUN5.js";
7
7
  import "./chunk-LVZTWUVJ.js";
8
8
  import "./chunk-2PCUAJJO.js";
9
- import "./chunk-VGVTIPFW.js";
9
+ import "./chunk-3IRX4B7Q.js";
10
10
  import "./chunk-3FYJM7LH.js";
11
11
  export {
12
12
  handleAsk
@@ -6,7 +6,7 @@ import {
6
6
  registerNote,
7
7
  registerTokens,
8
8
  searchRegister
9
- } from "./chunk-VGVTIPFW.js";
9
+ } from "./chunk-3IRX4B7Q.js";
10
10
  import "./chunk-3FYJM7LH.js";
11
11
  export {
12
12
  buildRegisterQuery,
@@ -121,7 +121,7 @@ function registerNote(rows) {
121
121
  ].join("\n");
122
122
  }
123
123
  function certificateLinks(rows) {
124
- const links = rows.filter((r) => r.pdf_key).map((r) => `- ${r.num}: https://www.oimlsmart.org/cert-pdf/${r.pdf_key}`);
124
+ const links = rows.filter((r) => r.pdf_key).map((r) => `- ${r.num}: https://www.ommisa.org/cert-pdf/${r.pdf_key}`);
125
125
  return links.length ? `The certificates' documents:
126
126
  ${links.join("\n")}` : "";
127
127
  }
@@ -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
@@ -817,8 +818,8 @@ var BaseUrnGenerator = class {
817
818
  const ed = this.maybe("edition");
818
819
  if (ed === void 0 || ed === null)
819
820
  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)}`;
821
+ const num2 = typeof ed === "object" && "number" in ed ? ed.number : ed;
822
+ return num2 === void 0 || num2 === null || num2 === "" ? void 0 : `ed.${String(num2)}`;
822
823
  }
823
824
  urnLanguage() {
824
825
  const langs = this.maybe("languages");
@@ -7139,9 +7140,9 @@ var IsoIdentifier = class _IsoIdentifier extends BaseIdentifier {
7139
7140
  }
7140
7141
  }
7141
7142
  }
7142
- const num = this.numberPortion(true);
7143
- if (num !== "")
7144
- result += result === "" ? num : ` ${num}`;
7143
+ const num2 = this.numberPortion(true);
7144
+ if (num2 !== "")
7145
+ result += result === "" ? num2 : ` ${num2}`;
7145
7146
  result += this.languagePortion(this.languageSingle());
7146
7147
  if (this.all_parts === true)
7147
7148
  result += " (all parts)";
@@ -7306,9 +7307,9 @@ var IsoSingleIdentifier = class extends IsoIdentifier {
7306
7307
  result += `${sep}${stage}`;
7307
7308
  }
7308
7309
  }
7309
- const num = this.numberPortion(true);
7310
- if (num !== "")
7311
- result += result === "" ? num : ` ${num}`;
7310
+ const num2 = this.numberPortion(true);
7311
+ if (num2 !== "")
7312
+ result += result === "" ? num2 : ` ${num2}`;
7312
7313
  result += this.languagePortion(this.languageSingle());
7313
7314
  if (this.all_parts === true)
7314
7315
  result += " (all parts)";
@@ -7611,11 +7612,11 @@ var IsoSupplementIdentifier = class extends IsoSingleIdentifier {
7611
7612
  const ts = this.typedStageState;
7612
7613
  const stage = ts === void 0 ? "" : stageAbbr(ts.entry, long, ts.originalAbbr);
7613
7614
  let result = `${baseStr}/${stage}`;
7614
- const num = this.numberPortion(true);
7615
- if (num !== "") {
7616
- if (!num.startsWith(":") && !stage.endsWith("."))
7615
+ const num2 = this.numberPortion(true);
7616
+ if (num2 !== "") {
7617
+ if (!num2.startsWith(":") && !stage.endsWith("."))
7617
7618
  result += " ";
7618
- result += num;
7619
+ result += num2;
7619
7620
  }
7620
7621
  result += this.languagePortion(this.languageSingle());
7621
7622
  return result;
@@ -7670,20 +7671,20 @@ var IsoDirectives = class extends IsoSingleIdentifier {
7670
7671
  const abbr = this.typedStageState === void 0 ? "" : stageAbbr(this.typedStageState.entry, false);
7671
7672
  if (abbr !== "")
7672
7673
  head += ` ${abbr}`;
7673
- let num = this.number ?? "";
7674
+ let num2 = this.number ?? "";
7674
7675
  if (this.part !== void 0)
7675
- num += ` ${this.part}`;
7676
+ num2 += ` ${this.part}`;
7676
7677
  if (this.subpart !== void 0)
7677
- num += `-${this.subpart}`;
7678
+ num2 += `-${this.subpart}`;
7678
7679
  if (this.stage_iteration !== void 0)
7679
- num += `.${this.stage_iteration.render() ?? ""}`;
7680
+ num2 += `.${this.stage_iteration.render() ?? ""}`;
7680
7681
  if (this.date !== void 0 && this.date.present())
7681
- num += `:${this.date.render()}`;
7682
- if (num === "")
7682
+ num2 += `:${this.date.render()}`;
7683
+ if (num2 === "")
7683
7684
  return head;
7684
- if (num.startsWith(":"))
7685
- return head + num;
7686
- return `${head} ${num}`;
7685
+ if (num2.startsWith(":"))
7686
+ return head + num2;
7687
+ return `${head} ${num2}`;
7687
7688
  }
7688
7689
  };
7689
7690
  registerType(IsoDirectives);
@@ -8606,9 +8607,9 @@ function preprocessNist(input) {
8606
8607
  cleaned = cleaned.replace(/(?<!e)(\d)(rev\d{4})/g, "$1 $2");
8607
8608
  cleaned = cleaned.replace(/(\d+e\d+)\.([A-Za-z]{3,9}\d{4})/g, "$1rev$2");
8608
8609
  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) => {
8610
+ cleaned = cleaned.replace(/(\d)r(\d{1,2})\/(\d{2,4})/g, (_m, num2, mon, yr) => {
8610
8611
  const yyyy = yr.length === 2 ? `19${yr}` : yr;
8611
- return `${num}/Upd1-${yyyy}${mon.padStart(2, "0")}`;
8612
+ return `${num2}/Upd1-${yyyy}${mon.padStart(2, "0")}`;
8612
8613
  });
8613
8614
  }
8614
8615
  if (!cleaned.includes("LCIRC") && !cleaned.includes("CIRC")) {
@@ -11321,17 +11322,17 @@ function normalizeJointStageSpellings(input) {
11321
11322
  let cleaned = input;
11322
11323
  const pubs = "((?:ISO/IEC/IEEE|IEEE/ISO/IEC|IEEE/IEC/ISO|ISO/IEEE|IEC/IEEE|IEEE/IEC|ISO/IEC)(?:/ ?| ))";
11323
11324
  const stage = "(FDIS|FCD|CDV|DIS\\d?|CD\\d?|WD|PWI|NP)";
11324
- const num = "(P?\\d+(?:[.-]\\d+)?)";
11325
+ const num2 = "(P?\\d+(?:[.-]\\d+)?)";
11325
11326
  const tail = "(,? (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Sept|Oct|Nov|Dec)[a-z]* \\d{4}|$|-\\d{4}(?:-\\d\\d)?)";
11326
11327
  cleaned = cleaned.replace(/(\d)_D(\d)/g, "$1/D$2");
11327
11328
  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");
11329
+ cleaned = cleaned.replace(new RegExp(`^${pubs}${num2}/(D[\\d.]+)/ ?${stage}\\b`), "$1$4 $2/$3");
11330
+ cleaned = cleaned.replace(new RegExp(`^${pubs}${num2}/ ${stage}${tail}`), "$1$3 $2$4");
11331
+ cleaned = cleaned.replace(new RegExp(`^${pubs}${num2}/${stage} (\\d{4})\\b`), "$1$3 $2-$4");
11332
+ cleaned = cleaned.replace(new RegExp(`^${pubs}${num2} ${stage}${tail}`), "$1$3 $2$4");
11333
+ cleaned = cleaned.replace(new RegExp(`^${pubs}${num2} ${stage} (D\\d+)\\b`), "$1$3 $2/$4");
11334
+ cleaned = cleaned.replace(new RegExp(`^${pubs}${num2}/ ?${stage}(-(?:19|20)\\d\\d(?:-\\d\\d)?)`), "$1$3 $2$4");
11335
+ cleaned = cleaned.replace(new RegExp(`^${pubs}${stage} ${num2}/? ?(D\\d+)\\b`), "$1$2 $3/$4");
11335
11336
  return cleaned;
11336
11337
  }
11337
11338
  function preprocessIeee(input) {
@@ -14556,8 +14557,8 @@ var CenBuilder = class {
14556
14557
  const numStr = (str3(d["number"]) ?? "").trim();
14557
14558
  if (!/^\d{1,6}$/.test(numStr))
14558
14559
  return void 0;
14559
- const num = Number.parseInt(numStr, 10);
14560
- if (!(num >= 6e4 && num <= 79999))
14560
+ const num2 = Number.parseInt(numStr, 10);
14561
+ if (!(num2 >= 6e4 && num2 <= 79999))
14561
14562
  return void 0;
14562
14563
  const impl = grammarImplementation("iec");
14563
14564
  if (impl === void 0)
@@ -19006,9 +19007,9 @@ var CsaAdoptedClass = (() => {
19006
19007
  baseStr = baseStr.replace(`:${year}`, `:${year.slice(2)}`);
19007
19008
  }
19008
19009
  }
19009
- baseStr = baseStr.replace(/\/Amd\s+(\d+)([:/-])(\d{2,4})\b/g, (_m, num, sep, year) => {
19010
+ baseStr = baseStr.replace(/\/Amd\s+(\d+)([:/-])(\d{2,4})\b/g, (_m, num2, sep, year) => {
19010
19011
  const short = year.length === 4 && (year.startsWith("20") || year.startsWith("19")) ? year.slice(2) : year;
19011
- return `/A${num}${sep}${short}`;
19012
+ return `/A${num2}${sep}${short}`;
19012
19013
  });
19013
19014
  let result;
19014
19015
  if (this.publisher_prefix !== void 0 && this.publisher_prefix.endsWith("-")) {
@@ -19635,9 +19636,9 @@ function parseCsa(input) {
19635
19636
  const full = Number(two) < 50 ? `20${two}` : `19${two}`;
19636
19637
  wrapped = wrapped.replace(new RegExp(`:${two}\\b`), `:${full}`);
19637
19638
  }
19638
- wrapped = wrapped.replace(/\/A(\d+)([:/-])(\d{2,4})\b/g, (_m, num, sep, year) => {
19639
+ wrapped = wrapped.replace(/\/A(\d+)([:/-])(\d{2,4})\b/g, (_m, num2, sep, year) => {
19639
19640
  const full = year.length === 2 ? Number(year) < 50 ? `20${year}` : `19${year}` : year;
19640
- return `/Amd ${num}${sep}${full}`;
19641
+ return `/Amd ${num2}${sep}${full}`;
19641
19642
  });
19642
19643
  const base = parseExternalStandard(wrapped);
19643
19644
  if (base === void 0) {
@@ -21908,13 +21909,13 @@ function parseOimlSpine(display) {
21908
21909
  const kind = String(h._type ?? "").split(":").pop() ?? "";
21909
21910
  const letter = TYPE_LETTER[kind] ?? "";
21910
21911
  if (!letter) return null;
21911
- const num = String(Number(h.number));
21912
+ const num2 = String(Number(h.number));
21912
21913
  const part = h.part !== void 0 ? String(h.part) : void 0;
21913
21914
  const ed = h.year !== void 0 ? String(h.year) : h.edition !== void 0 ? String(h.edition) : void 0;
21914
21915
  return {
21915
- doc_number: num,
21916
+ doc_number: num2,
21916
21917
  ...ed ? { edition: ed } : {},
21917
- label: `OIML ${letter} ${num}${part ? `-${part}` : ""}${ed ? `:${ed}` : ""}`
21918
+ label: `OIML ${letter} ${num2}${part ? `-${part}` : ""}${ed ? `:${ed}` : ""}`
21918
21919
  };
21919
21920
  } catch {
21920
21921
  return null;
@@ -21949,9 +21950,9 @@ var oimlPubid = {
21949
21950
  for (const m of query.matchAll(re)) {
21950
21951
  const [, oimlPrefix, letter, gap, digits, edition] = m;
21951
21952
  if (digits.length === 1 && !oimlPrefix && !gap) continue;
21952
- const num = String(Number(digits));
21953
+ const num2 = String(Number(digits));
21953
21954
  const type = letter.toUpperCase();
21954
- return { doc_number: num, ...edition ? { edition } : {}, label: `OIML ${type} ${num}${edition ? `:${edition}` : ""}` };
21955
+ return { doc_number: num2, ...edition ? { edition } : {}, label: `OIML ${type} ${num2}${edition ? `:${edition}` : ""}` };
21955
21956
  }
21956
21957
  return null;
21957
21958
  },
@@ -24578,6 +24579,32 @@ function telemetry(env, ctx, tier, route, model, ok, answerChars, queryHash, lan
24578
24579
  ])
24579
24580
  );
24580
24581
  }
24582
+ function estimateTokens(chars) {
24583
+ return Math.ceil(chars / 4);
24584
+ }
24585
+ function tokenTier(bucketId) {
24586
+ return bucketId.startsWith("key:") ? "key" : bucketId.startsWith("sub:") ? "member" : "anon";
24587
+ }
24588
+ function tokenLimit(env, bucketId) {
24589
+ const tier = tokenTier(bucketId);
24590
+ return tier === "key" ? num(env, "KEY_DAY_TOKENS", 4e6) : tier === "member" ? num(env, "MEMBER_DAY_TOKENS", 15e5) : num(env, "ANON_DAY_TOKENS", 3e5);
24591
+ }
24592
+ async function tokenBudget(env, bucketId) {
24593
+ const used = Number(await env.CACHE.get(`t:${today()}:ask:${await sha256Hex(bucketId)}`) ?? "0");
24594
+ return { used, limit: tokenLimit(env, bucketId) };
24595
+ }
24596
+ async function chargeTokens(env, bucketId, tokens) {
24597
+ if (!(tokens > 0)) return;
24598
+ await kvIncr(env.CACHE, `t:${today()}:ask:${await sha256Hex(bucketId)}`, tokens);
24599
+ }
24600
+ function usageTotal(usage) {
24601
+ if (!usage || typeof usage !== "object") return null;
24602
+ const u = usage;
24603
+ const p = Number(u.prompt_tokens ?? 0);
24604
+ const c = Number(u.completion_tokens ?? 0);
24605
+ const total = p + c;
24606
+ return Number.isFinite(total) && total > 0 ? total : null;
24607
+ }
24581
24608
 
24582
24609
  // workers/worker_public/src/graph.ts
24583
24610
  function docNumberOf(nodeId) {
@@ -24662,6 +24689,10 @@ export {
24662
24689
  clientIp,
24663
24690
  checkQuota,
24664
24691
  telemetry,
24692
+ estimateTokens,
24693
+ tokenBudget,
24694
+ chargeTokens,
24695
+ usageTotal,
24665
24696
  graphExpand,
24666
24697
  editionNote
24667
24698
  };
@@ -7,7 +7,7 @@ import {
7
7
  sessionFrom,
8
8
  telemetry,
9
9
  understandQuery
10
- } from "./chunk-TPV5GPY2.js";
10
+ } from "./chunk-LL3LWBZH.js";
11
11
  import {
12
12
  corsHeaders,
13
13
  err,
@@ -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-LL3LWBZH.js";
39
43
  import {
40
44
  corsHeaders,
41
45
  err,
@@ -68,7 +72,7 @@ import {
68
72
  certificateLinks,
69
73
  registerNote,
70
74
  searchRegister
71
- } from "./chunk-VGVTIPFW.js";
75
+ } from "./chunk-3IRX4B7Q.js";
72
76
  import {
73
77
  P
74
78
  } from "./chunk-3FYJM7LH.js";
@@ -1892,7 +1896,7 @@ async function runTool(db, call) {
1892
1896
  if (call.name !== "register_search") return null;
1893
1897
  const query = String(call.args?.query ?? "").trim().slice(0, 160);
1894
1898
  if (!query) return null;
1895
- const { searchRegister: searchRegister2, registerNote: registerNote2 } = await import("./certificates-DNRUOYRF.js");
1899
+ const { searchRegister: searchRegister2, registerNote: registerNote2 } = await import("./certificates-EMHFAUWC.js");
1896
1900
  const reg = await searchRegister2(db, query, true);
1897
1901
  const output = reg?.rows?.length ? registerNote2(reg.rows) : `No certificate was found for "${query}" in the certificates database (the register snapshot). State this as the search's result, with the searched string visible.`;
1898
1902
  return { name: call.name, query, output };
@@ -1929,17 +1933,59 @@ var ACTION_STEMS = [
1929
1933
  "announc",
1930
1934
  "download"
1931
1935
  ];
1936
+ var STOP_WORDS = /* @__PURE__ */ new Set([
1937
+ "which",
1938
+ "what",
1939
+ "where",
1940
+ "when",
1941
+ "how",
1942
+ "why",
1943
+ "who",
1944
+ "does",
1945
+ "did",
1946
+ "can",
1947
+ "the",
1948
+ "and",
1949
+ "for",
1950
+ "with",
1951
+ "that",
1952
+ "this",
1953
+ "are",
1954
+ "was",
1955
+ "were",
1956
+ "has",
1957
+ "have",
1958
+ "had",
1959
+ "not",
1960
+ "but",
1961
+ "all",
1962
+ "any",
1963
+ "platform",
1964
+ "operation",
1965
+ "operations",
1966
+ "endpoint",
1967
+ "endpoints",
1968
+ "api"
1969
+ ]);
1970
+ function anchorWords(anchor) {
1971
+ return anchor.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
1972
+ }
1973
+ var singular = (w) => w.length > 3 && w.endsWith("s") ? w.slice(0, -1) : w;
1932
1974
  function matchOperations(query, rows) {
1933
1975
  const q = query.toLowerCase();
1934
1976
  const stems = ACTION_STEMS.filter((s) => q.includes(s));
1935
- if (!stems.length) return [];
1977
+ const objects = query.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 2 && !STOP_WORDS.has(w)).map(singular);
1936
1978
  const scored = [];
1937
1979
  for (const r of rows) {
1938
1980
  const hay = `${r.anchor} ${r.text}`.toLowerCase();
1939
- const score = stems.filter((s) => hay.includes(s)).length;
1940
- if (score > 0) scored.push({ ...r, score });
1981
+ const verbScore = stems.filter((s) => hay.includes(s)).length * 3;
1982
+ const words = anchorWords(r.anchor).map(singular);
1983
+ const objectScore = objects.filter((o) => words.includes(o)).length * 10;
1984
+ if (objectScore + verbScore > 0) scored.push({ ...r, score: objectScore + verbScore, objectScore });
1941
1985
  }
1942
- return scored.sort((a, b) => b.score - a.score).slice(0, 4);
1986
+ const named = scored.filter((s) => s.objectScore > 0);
1987
+ const pool = named.length ? named : scored;
1988
+ return pool.sort((a, b) => b.score - a.score).slice(0, 4);
1943
1989
  }
1944
1990
  function catalogNote(anchors) {
1945
1991
  if (!anchors.length) return "";
@@ -1949,12 +1995,12 @@ ${anchors.map((a) => `- ${a}`).join("\n")}`;
1949
1995
  async function operationsCatalogNote(db, query) {
1950
1996
  if (!isOperationIntent(query)) return void 0;
1951
1997
  try {
1952
- const rows = (await db.prepare("SELECT clause_anchor AS anchor, substr(text, 1, 220) AS text FROM chunks WHERE corpus = 'smart-ops' AND clause_anchor IS NOT NULL ORDER BY clause_anchor").all()).results ?? [];
1998
+ 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 ?? [];
1953
1999
  const opRows = rows.map((r) => ({ anchor: String(r.anchor ?? ""), text: String(r.text ?? "") })).filter((r) => r.anchor);
1954
2000
  const matched = matchOperations(query, opRows);
1955
2001
  const parts = [];
1956
2002
  if (matched.length) {
1957
- parts.push(`Operations whose description matches the question's action (AUTHORITATIVE \u2014 if one matches, the answer MUST name it and cite its line; never answer that the information is missing):
2003
+ parts.push(`Operations whose name or description matches the question (AUTHORITATIVE \u2014 if one matches, the answer MUST name it and cite its line; never answer that the information is missing):
1958
2004
  ${matched.map((m) => `- ${m.text}`).join("\n")}`);
1959
2005
  }
1960
2006
  parts.push(catalogNote(opRows.map((r) => r.anchor)));
@@ -2124,7 +2170,23 @@ async function summarizeHistory(env, model, turns) {
2124
2170
  return null;
2125
2171
  }
2126
2172
  }
2127
- async function* sseTokens(stream) {
2173
+ function messageChars(messages) {
2174
+ let n = 0;
2175
+ for (const m of messages) {
2176
+ if (typeof m.content === "string") n += m.content.length;
2177
+ else if (Array.isArray(m.content)) {
2178
+ for (const part of m.content) if (part?.type === "text" && typeof part.text === "string") n += part.text.length;
2179
+ }
2180
+ }
2181
+ return n;
2182
+ }
2183
+ async function chargeAnswerTokens(env, bucketId, stats, promptChars, answerChars) {
2184
+ const total = usageTotal(stats?.usage) ?? estimateTokens(promptChars + answerChars);
2185
+ await chargeTokens(env, bucketId, total);
2186
+ const budget = await tokenBudget(env, bucketId);
2187
+ return { tokens_used: budget.used, token_limit: budget.limit };
2188
+ }
2189
+ async function* sseTokens(stream, stats) {
2128
2190
  const reader = stream.getReader();
2129
2191
  const decoder = new TextDecoder();
2130
2192
  let buf = "";
@@ -2142,6 +2204,7 @@ async function* sseTokens(stream) {
2142
2204
  try {
2143
2205
  const evt = JSON.parse(payload);
2144
2206
  const tok = typeof evt?.response === "string" ? evt.response : evt?.choices?.[0]?.delta?.content;
2207
+ if (evt.usage && stats && stats.usage === void 0) stats.usage = evt.usage;
2145
2208
  if (tok) yield tok;
2146
2209
  } catch {
2147
2210
  }
@@ -2185,6 +2248,12 @@ async function handleAsk(env, ctx, req, tier, key) {
2185
2248
  if (!quota.ok) {
2186
2249
  return err(429, "quota_exceeded", `Daily question limit reached (${quota.limit}). Try again tomorrow.`);
2187
2250
  }
2251
+ const tokBudget = await tokenBudget(env, bucketId);
2252
+ if (tokBudget.used >= tokBudget.limit) {
2253
+ return err(429, "quota_exceeded", `Daily token budget reached (${tokBudget.limit} tokens). Try again tomorrow.`);
2254
+ }
2255
+ quota.tokens_used = tokBudget.used;
2256
+ quota.token_limit = tokBudget.limit;
2188
2257
  const hardCap = num(env, "ANON_DAY_HARD_CAP", 5e3);
2189
2258
  if (tier === "anon" && quota.used > hardCap) {
2190
2259
  return err(503, "generation_disabled", "Generation is temporarily paused; search remains available.");
@@ -2402,14 +2471,17 @@ ${summary}` }] : [],
2402
2471
  `));
2403
2472
  if (readAs()) send({ type: "read", read: readAs() });
2404
2473
  send({ type: "citations", citations: [], context_applied: NO_CONTEXT, quota });
2474
+ const stats = {};
2405
2475
  let full = "";
2406
2476
  try {
2407
- for await (const tok of sseTokens(stream)) {
2477
+ for await (const tok of sseTokens(stream, stats)) {
2408
2478
  full += tok;
2409
2479
  send({ type: "token", v: tok });
2410
2480
  }
2411
2481
  } catch {
2412
2482
  }
2483
+ const tq2 = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages2), full.length);
2484
+ send({ type: "quota", quota: { ...quota, ...tq2 } });
2413
2485
  send({ type: "done", model, query_hash: queryHash2, context_applied: NO_CONTEXT, read: readAs() });
2414
2486
  telemetry(env, ctx, tier, "ask", model, true, full.length, queryHash2, q.lang, void 0, telemetryMeta());
2415
2487
  controller.close();
@@ -2465,7 +2537,8 @@ ${summary}` }] : [],
2465
2537
  corsHeaders(req)
2466
2538
  );
2467
2539
  }
2468
- return json({ answer: verdict.answer, citations: citations2, model, query_hash: queryHash2, follow_ups: [], context_applied: draftCtxApplied, read: readAs(), ...draftPayload ? { draft: draftPayload } : {}, quota });
2540
+ const tq2 = await chargeAnswerTokens(env, bucketId, void 0, q.query.length, verdict.answer.length);
2541
+ return json({ answer: verdict.answer, citations: citations2, model, query_hash: queryHash2, follow_ups: [], context_applied: draftCtxApplied, read: readAs(), ...draftPayload ? { draft: draftPayload } : {}, quota: { ...quota, ...tq2 } });
2469
2542
  }
2470
2543
  if (apiCallIntent) {
2471
2544
  const callCtxApplied = declaredCtx ? appliedContext(declaredCtx, null) : NO_CONTEXT;
@@ -2492,7 +2565,8 @@ ${summary}` }] : [],
2492
2565
  corsHeaders(req)
2493
2566
  );
2494
2567
  }
2495
- return json({ answer: verdict.answer, citations: [], model, query_hash: queryHash2, follow_ups: [], context_applied: callCtxApplied, read: readAs(), ...draftPayload ? { draft: draftPayload } : {}, quota });
2568
+ const tq2 = await chargeAnswerTokens(env, bucketId, void 0, q.query.length, verdict.answer.length);
2569
+ return json({ answer: verdict.answer, citations: [], model, query_hash: queryHash2, follow_ups: [], context_applied: callCtxApplied, read: readAs(), ...draftPayload ? { draft: draftPayload } : {}, quota: { ...quota, ...tq2 } });
2496
2570
  }
2497
2571
  let liveRecords;
2498
2572
  let accountNote;
@@ -2792,14 +2866,17 @@ ${q.query}` }],
2792
2866
  send({ type: "read", read: readAs() });
2793
2867
  const sourceQuality = answerQuality(cites.map((c) => c.quality));
2794
2868
  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 });
2869
+ const stats = {};
2795
2870
  let full = "";
2796
2871
  try {
2797
- for await (const tok of sseTokens(stream)) {
2872
+ for await (const tok of sseTokens(stream, stats)) {
2798
2873
  full += tok;
2799
2874
  send({ type: "token", v: tok });
2800
2875
  }
2801
2876
  } catch {
2802
2877
  }
2878
+ const tq2 = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages), full.length);
2879
+ send({ type: "quota", quota: { ...quota, ...tq2 } });
2803
2880
  const canonical0 = canonicalRefusal(full);
2804
2881
  const c2 = canonical0.includes(refusalAnswer()) ? { text: canonical0, blocks: [], dropped: [] } : await contractV2(env.DB, canonical0, usedHits);
2805
2882
  send({
@@ -2965,7 +3042,8 @@ ${q.query}` }],
2965
3042
  text: h.text.slice(0, 1200),
2966
3043
  ...h.metadata.table_selection ? { sel: h.metadata.table_selection } : {}
2967
3044
  }));
2968
- return json({ ...out, context: contextOut, read: readAs(), quota }, 200, { ...corsHeaders(req), "server-timing": serverTiming() });
3045
+ const tq = await chargeAnswerTokens(env, bucketId, void 0, messageChars(messages), answer.length);
3046
+ return json({ ...out, context: contextOut, read: readAs(), quota: { ...quota, ...tq } }, 200, { ...corsHeaders(req), "server-timing": serverTiming() });
2969
3047
  }
2970
3048
  function sseResponse(events, cors) {
2971
3049
  const encoder = new TextEncoder();
@@ -1,4 +1,8 @@
1
1
  export declare function isOperationIntent(query: string): boolean;
2
+ /** camelCase/snake anchors meet the question's words: "updates an
3
+ * entity record" must reach putEntityRecord even when its description
4
+ * words the action differently ("replaces the stored…"). */
5
+ export declare function anchorWords(anchor: string): string[];
2
6
  export declare function matchOperations(query: string, rows: {
3
7
  anchor: string;
4
8
  text: string;
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-OXTRNMTZ.js";
4
+ import "./chunk-LL3LWBZH.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-EOQVQBS6.js";
9
+ } from "../../chunk-V7PEVNNO.js";
10
10
  import {
11
11
  handleSearch
12
- } from "../../chunk-FLNX4O7C.js";
12
+ } from "../../chunk-OXTRNMTZ.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-LL3LWBZH.js";
39
39
  import {
40
40
  authenticate,
41
41
  corsHeaders,
@@ -63,7 +63,7 @@ import {
63
63
  sha256Hex,
64
64
  today
65
65
  } from "../../chunk-2PCUAJJO.js";
66
- import "../../chunk-VGVTIPFW.js";
66
+ import "../../chunk-3IRX4B7Q.js";
67
67
  import {
68
68
  P,
69
69
  setProfile
@@ -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-OUNG3JIC.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-MFFOHVQ2.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-LN5Q7OSZ.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.71",
30
+ "version": "0.2.73",
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",
@@ -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,7 +206,28 @@ 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 = "";
@@ -227,6 +248,7 @@ async function* sseTokens(stream: ReadableStream<Uint8Array>): AsyncGenerator<st
227
248
  typeof evt?.response === "string"
228
249
  ? evt.response
229
250
  : evt?.choices?.[0]?.delta?.content;
251
+ if (evt.usage && stats && stats.usage === undefined) stats.usage = evt.usage;
230
252
  if (tok) yield tok;
231
253
  } catch {
232
254
  // partial JSON in line splitting — ignore
@@ -320,6 +342,14 @@ async function handleAsk(
320
342
  if (!quota.ok) {
321
343
  return err(429, "quota_exceeded", `Daily question limit reached (${quota.limit}). Try again tomorrow.`);
322
344
  }
345
+ // the real allowance is the day's TOKENS (the 2026-09-29 shift): the
346
+ // request cap above is the anti-abuse backstop, this is the budget
347
+ const tokBudget = await tokenBudget(env, bucketId);
348
+ if (tokBudget.used >= tokBudget.limit) {
349
+ return err(429, "quota_exceeded", `Daily token budget reached (${tokBudget.limit} tokens). Try again tomorrow.`);
350
+ }
351
+ (quota as any).tokens_used = tokBudget.used;
352
+ (quota as any).token_limit = tokBudget.limit;
323
353
 
324
354
  const hardCap = num(env as any, "ANON_DAY_HARD_CAP", 5000);
325
355
  if (tier === "anon" && quota.used > hardCap) {
@@ -668,15 +698,18 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
668
698
  // the reading arrives first — a conversational turn is still a reading
669
699
  if (readAs()) send({ type: "read", read: readAs() });
670
700
  send({ type: "citations", citations: [], context_applied: NO_CONTEXT, quota, });
701
+ const stats: { usage?: unknown } = {};
671
702
  let full = "";
672
703
  try {
673
- for await (const tok of sseTokens(stream)) {
704
+ for await (const tok of sseTokens(stream, stats)) {
674
705
  full += tok;
675
706
  send({ type: "token", v: tok });
676
707
  }
677
708
  } catch {
678
709
  // stream ended prematurely — deliver what we have
679
710
  }
711
+ const tq = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages), full.length);
712
+ send({ type: "quota", quota: { ...quota, ...tq } });
680
713
  send({ type: "done", model, query_hash: queryHash, context_applied: NO_CONTEXT, read: readAs() });
681
714
  telemetry(env, ctx, tier, "ask", model, true, full.length, queryHash, q.lang, undefined, telemetryMeta());
682
715
  controller.close();
@@ -748,7 +781,8 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
748
781
  corsHeaders(req),
749
782
  );
750
783
  }
751
- return json({ answer: verdict.answer, citations, model, query_hash: queryHash, follow_ups: [], context_applied: draftCtxApplied, read: readAs(), ...(draftPayload ? { draft: draftPayload } : {}), quota, });
784
+ const tq = await chargeAnswerTokens(env, bucketId, undefined, q.query.length, verdict.answer.length);
785
+ return json({ answer: verdict.answer, citations, model, query_hash: queryHash, follow_ups: [], context_applied: draftCtxApplied, read: readAs(), ...(draftPayload ? { draft: draftPayload } : {}), quota: { ...quota, ...tq } });
752
786
  }
753
787
 
754
788
  // ── The api_call draft (TODO.ai-platform/09) — the operations
@@ -785,7 +819,8 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
785
819
  corsHeaders(req),
786
820
  );
787
821
  }
788
- return json({ answer: verdict.answer, citations: [], model, query_hash: queryHash, follow_ups: [], context_applied: callCtxApplied, read: readAs(), ...(draftPayload ? { draft: draftPayload } : {}), quota, });
822
+ const tq = await chargeAnswerTokens(env, bucketId, undefined, q.query.length, verdict.answer.length);
823
+ return json({ answer: verdict.answer, citations: [], model, query_hash: queryHash, follow_ups: [], context_applied: callCtxApplied, read: readAs(), ...(draftPayload ? { draft: draftPayload } : {}), quota: { ...quota, ...tq } });
789
824
  }
790
825
 
791
826
  // (declared before the retrieval try: the account block, the refusal
@@ -1227,15 +1262,18 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1227
1262
  send({ type: "read", read: readAs() });
1228
1263
  const sourceQuality = answerQuality(cites.map((c: any) => c.quality));
1229
1264
  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, });
1265
+ const stats: { usage?: unknown } = {};
1230
1266
  let full = "";
1231
1267
  try {
1232
- for await (const tok of sseTokens(stream)) {
1268
+ for await (const tok of sseTokens(stream, stats)) {
1233
1269
  full += tok;
1234
1270
  send({ type: "token", v: tok });
1235
1271
  }
1236
1272
  } catch {
1237
1273
  // stream ended prematurely — deliver what we have
1238
1274
  }
1275
+ const tq = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages), full.length);
1276
+ send({ type: "quota", quota: { ...quota, ...tq } });
1239
1277
  const canonical0 = canonicalRefusal(full);
1240
1278
  // answer contract v2: validate [[u:]] refs, resolve typed blocks
1241
1279
  const c2 = canonical0.includes(refusalAnswer())
@@ -1475,7 +1513,8 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1475
1513
  text: h.text.slice(0, 1200),
1476
1514
  ...((h.metadata as any).table_selection ? { sel: (h.metadata as any).table_selection } : {}),
1477
1515
  }));
1478
- return json({ ...out, context: contextOut, read: readAs(), quota }, 200, { ...corsHeaders(req), "server-timing": serverTiming() });
1516
+ const tq = await chargeAnswerTokens(env, bucketId, undefined, messageChars(messages), answer.length);
1517
+ return json({ ...out, context: contextOut, read: readAs(), quota: { ...quota, ...tq } }, 200, { ...corsHeaders(req), "server-timing": serverTiming() });
1479
1518
  }
1480
1519
 
1481
1520
  function sseResponse(events: unknown[], cors: Record<string, string>): Response {
@@ -134,7 +134,7 @@ export function registerNote(rows: RegisterRow[]): string {
134
134
  * is in the R2 plane (TODO 7) — the user gets the document, not just
135
135
  * the fact. */
136
136
  export function certificateLinks(rows: RegisterRow[]): string {
137
- const links = rows.filter((r) => r.pdf_key).map((r) => `- ${r.num}: https://www.oimlsmart.org/cert-pdf/${r.pdf_key}`);
137
+ const links = rows.filter((r) => r.pdf_key).map((r) => `- ${r.num}: https://www.ommisa.org/cert-pdf/${r.pdf_key}`);
138
138
  return links.length ? `The certificates' documents:\n${links.join("\n")}` : "";
139
139
  }
140
140
 
@@ -1,10 +1,11 @@
1
1
  // The operations catalog lane (TODO.new-era/6's platform surface): the
2
2
  // smart-ops corpus is SMALL (169 operations) and name-addressable. An
3
- // operation-intent question gets (1) the operations whose description
4
- // matches the question's action stems — with their full lines, and (2)
5
- // the complete name index. The note is AUTHORITATIVE: the model must
6
- // name the operation from it, never answer from the passages' silence.
7
- // Pure composition lives here; the D1 call is a thin wrapper.
3
+ // operation-intent question gets (1) the operations whose NAME carries
4
+ // the question's object words and whose description carries its action
5
+ // stems — with their full lines, and (2) the complete name index. The
6
+ // note is AUTHORITATIVE: the model must name the operation from it,
7
+ // never answer from the passages' silence. Pure composition lives
8
+ // here; the D1 call is a thin wrapper.
8
9
 
9
10
  export function isOperationIntent(query: string): boolean {
10
11
  return /\b(operations?|endpoints?)\b/i.test(query);
@@ -18,17 +19,48 @@ const ACTION_STEMS = [
18
19
  "confirm", "approv", "declin", "decide", "revok", "grant", "notify", "advanc", "announc", "download",
19
20
  ];
20
21
 
22
+ const STOP_WORDS = new Set([
23
+ "which", "what", "where", "when", "how", "why", "who", "does", "did", "can", "the", "and", "for",
24
+ "with", "that", "this", "are", "was", "were", "has", "have", "had", "not", "but", "all", "any",
25
+ "platform", "operation", "operations", "endpoint", "endpoints", "api",
26
+ ]);
27
+
28
+ /** camelCase/snake anchors meet the question's words: "updates an
29
+ * entity record" must reach putEntityRecord even when its description
30
+ * words the action differently ("replaces the stored…"). */
31
+ export function anchorWords(anchor: string): string[] {
32
+ return anchor
33
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
34
+ .toLowerCase()
35
+ .split(/[^a-z0-9]+/)
36
+ .filter(Boolean);
37
+ }
38
+
39
+ const singular = (w: string) => (w.length > 3 && w.endsWith("s") ? w.slice(0, -1) : w);
40
+
21
41
  export function matchOperations(query: string, rows: { anchor: string; text: string }[]): { anchor: string; text: string }[] {
22
42
  const q = query.toLowerCase();
23
43
  const stems = ACTION_STEMS.filter((s) => q.includes(s));
24
- if (!stems.length) return [];
25
- const scored: { anchor: string; text: string; score: number }[] = [];
44
+ const objects = query
45
+ .toLowerCase()
46
+ .split(/[^a-z0-9]+/)
47
+ .filter((w) => w.length > 2 && !STOP_WORDS.has(w))
48
+ .map(singular);
49
+ const scored: { anchor: string; text: string; score: number; objectScore: number }[] = [];
26
50
  for (const r of rows) {
27
51
  const hay = `${r.anchor} ${r.text}`.toLowerCase();
28
- const score = stems.filter((s) => hay.includes(s)).length;
29
- if (score > 0) scored.push({ ...r, score });
52
+ const verbScore = stems.filter((s) => hay.includes(s)).length * 3;
53
+ const words = anchorWords(r.anchor).map(singular);
54
+ const objectScore = objects.filter((o) => words.includes(o)).length * 10;
55
+ if (objectScore + verbScore > 0) scored.push({ ...r, score: objectScore + verbScore, objectScore });
30
56
  }
31
- return scored.sort((a, b) => b.score - a.score).slice(0, 4);
57
+ // A question that names its object ("entity record") has found its
58
+ // target by name — verb-only mentions are the noise that displaced
59
+ // the answer with an unrelated sibling (the 2026-09-29 "blob upload"
60
+ // answers).
61
+ const named = scored.filter((s) => s.objectScore > 0);
62
+ const pool = named.length ? named : scored;
63
+ return pool.sort((a, b) => b.score - a.score).slice(0, 4);
32
64
  }
33
65
 
34
66
  export function catalogNote(anchors: string[]): string {
@@ -39,12 +71,12 @@ export function catalogNote(anchors: string[]): string {
39
71
  export async function operationsCatalogNote(db: any, query: string): Promise<string | undefined> {
40
72
  if (!isOperationIntent(query)) return undefined;
41
73
  try {
42
- const rows = (await db.prepare("SELECT clause_anchor AS anchor, substr(text, 1, 220) AS text FROM chunks WHERE corpus = 'smart-ops' AND clause_anchor IS NOT NULL ORDER BY clause_anchor").all()).results ?? [];
74
+ 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 ?? [];
43
75
  const opRows = (rows as any[]).map((r) => ({ anchor: String(r.anchor ?? ""), text: String(r.text ?? "") })).filter((r) => r.anchor);
44
76
  const matched = matchOperations(query, opRows);
45
77
  const parts: string[] = [];
46
78
  if (matched.length) {
47
- parts.push(`Operations whose description matches the question's action (AUTHORITATIVE — if one matches, the answer MUST name it and cite its line; never answer that the information is missing):\n${matched.map((m) => `- ${m.text}`).join("\n")}`);
79
+ parts.push(`Operations whose name or description matches the question (AUTHORITATIVE — if one matches, the answer MUST name it and cite its line; never answer that the information is missing):\n${matched.map((m) => `- ${m.text}`).join("\n")}`);
48
80
  }
49
81
  parts.push(catalogNote(opRows.map((r) => r.anchor)));
50
82
  return parts.filter(Boolean).join("\n\n") || undefined;
@@ -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
+ }