@konneal/engine 0.2.32 → 0.2.34

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.
Files changed (34) hide show
  1. package/dist/apicalls.d.ts +72 -0
  2. package/dist/ask-BDD5TFNC.js +12 -0
  3. package/dist/{chunk-JGKSUSF5.js → chunk-4GJGBGJK.js} +3 -3
  4. package/dist/{chunk-F66NYFUK.js → chunk-DADOI4Q6.js} +44 -8
  5. package/dist/{chunk-TDKNQPA2.js → chunk-MHA2OFWE.js} +305 -11
  6. package/dist/{chunk-S7NLWPNX.js → chunk-MNNE7OIM.js} +4 -4
  7. package/dist/{chunk-2RKYO3OC.js → chunk-R2V3X6SQ.js} +1 -1
  8. package/dist/{chunk-OMXAE27N.js → chunk-V46XM2GU.js} +4 -0
  9. package/dist/config.d.ts +1 -0
  10. package/dist/context.d.ts +37 -0
  11. package/dist/drafts.d.ts +5 -0
  12. package/dist/env.d.ts +2 -0
  13. package/dist/quota.d.ts +1 -0
  14. package/dist/requestScope.d.ts +10 -2
  15. package/dist/search-EADF54TY.js +12 -0
  16. package/dist/worker_mcp/src/index.js +2 -2
  17. package/dist/worker_public/src/config.js +1 -1
  18. package/dist/worker_public/src/index.js +12 -9
  19. package/dist/worker_public/src/requestScope.js +2 -2
  20. package/docs/API.md +83 -0
  21. package/package.json +1 -1
  22. package/workers/worker_public/migrations/0015_ask_retries.sql +4 -0
  23. package/workers/worker_public/src/apicalls.ts +447 -0
  24. package/workers/worker_public/src/ask.ts +57 -7
  25. package/workers/worker_public/src/config.ts +4 -0
  26. package/workers/worker_public/src/context.ts +89 -4
  27. package/workers/worker_public/src/drafts.ts +10 -0
  28. package/workers/worker_public/src/env.ts +5 -1
  29. package/workers/worker_public/src/index.ts +4 -1
  30. package/workers/worker_public/src/ports/cloudflare/adapters.ts +8 -0
  31. package/workers/worker_public/src/quota.ts +3 -3
  32. package/workers/worker_public/src/requestScope.ts +9 -3
  33. package/dist/ask-43C2WZZO.js +0 -12
  34. package/dist/search-NHTT5DDI.js +0 -12
@@ -7,24 +7,24 @@ import {
7
7
  sessionFrom,
8
8
  telemetry,
9
9
  understandQuery
10
- } from "./chunk-F66NYFUK.js";
10
+ } from "./chunk-DADOI4Q6.js";
11
11
  import {
12
12
  corsHeaders,
13
13
  err,
14
14
  json,
15
15
  readJson,
16
16
  validateQuery
17
- } from "./chunk-2RKYO3OC.js";
17
+ } from "./chunk-R2V3X6SQ.js";
18
18
  import {
19
19
  entitlementScope,
20
20
  standardKeysFrom
21
- } from "./chunk-JGKSUSF5.js";
21
+ } from "./chunk-4GJGBGJK.js";
22
22
  import {
23
23
  LIMITS,
24
24
  MODELS,
25
25
  num,
26
26
  sha256Hex
27
- } from "./chunk-OMXAE27N.js";
27
+ } from "./chunk-V46XM2GU.js";
28
28
 
29
29
  // workers/worker_public/src/search.ts
30
30
  async function handleSearch(env, ctx, req, tier, key) {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  LIMITS,
3
3
  sha256Hex
4
- } from "./chunk-OMXAE27N.js";
4
+ } from "./chunk-V46XM2GU.js";
5
5
  import {
6
6
  P
7
7
  } from "./chunk-3FYJM7LH.js";
@@ -13,6 +13,10 @@ var MODELS = {
13
13
  member: "@cf/zai-org/glm-5.3-flash",
14
14
  // hot-path understanding/summarize stays on the cheap Qwen (cost-first lane)
15
15
  understand: "@cf/qwen/qwen3-30b-a3b-fp8",
16
+ // the api_call act's intent-to-act mapping (TODO.ai-platform/09) — the
17
+ // operations deployment points ACTS_MODEL at the stronger reasoning
18
+ // tier; the floor is the understand lane
19
+ acts: "@cf/qwen/qwen3-30b-a3b-fp8",
16
20
  // generation fallback when the answer model is unavailable
17
21
  fallback: "@cf/qwen/qwen3-30b-a3b-fp8",
18
22
  grader: "@cf/deepseek-ai/deepseek-v4-flash-0731",
package/dist/config.d.ts CHANGED
@@ -4,6 +4,7 @@ export declare const MODELS: {
4
4
  readonly anon: "@cf/zai-org/glm-5.3-flash";
5
5
  readonly member: "@cf/zai-org/glm-5.3-flash";
6
6
  readonly understand: "@cf/qwen/qwen3-30b-a3b-fp8";
7
+ readonly acts: "@cf/qwen/qwen3-30b-a3b-fp8";
7
8
  readonly fallback: "@cf/qwen/qwen3-30b-a3b-fp8";
8
9
  readonly grader: "@cf/deepseek-ai/deepseek-v4-flash-0731";
9
10
  readonly enrich: "@cf/deepseek-ai/deepseek-v4-pro-0813";
package/dist/context.d.ts CHANGED
@@ -1,3 +1,25 @@
1
+ /** One act the declared entity's lifecycle machine offers at its current
2
+ * state, FOR THE DECLARING USER'S ROLE (the platform filters before
3
+ * publishing; the wire is advisory — the platform re-validates every
4
+ * proposed act authoritatively on the way back). */
5
+ export interface MachineAct {
6
+ /** the machine's own action id (the model's vocabulary) */
7
+ action: string;
8
+ /** the state the act lands the entity on */
9
+ to: string;
10
+ /** the declared guard (the caller-supplied computed input the
11
+ * transition requires), when the transition declares one */
12
+ guard?: string;
13
+ }
14
+ /** The affordance channel (TODO.ai-platform/08): the entity declaration's
15
+ * machine facet — the lifecycle machine's current state (the fact,
16
+ * published even when the role fires nothing) + the acts offered to the
17
+ * declaring user's role (EMPTY means "the user can read but not act").
18
+ * Only ever present on the entity kind. */
19
+ export interface MachineContext {
20
+ state: string;
21
+ acts: MachineAct[];
22
+ }
1
23
  export interface DeclaredContext {
2
24
  kind: "page" | "entity" | "document" | "account";
3
25
  /** display label ("this certificate R60/2021-A-EX1-26.01") — echoed
@@ -10,6 +32,8 @@ export interface DeclaredContext {
10
32
  * (urn:oiml:pub:r:60-1:2021) or the plain docidentifier */
11
33
  doc?: string;
12
34
  edition?: string;
35
+ /** the affordance channel's machine facet (entity kind only) */
36
+ machine?: MachineContext;
13
37
  }
14
38
  /** The account context's live-read echo (TODO.ai-platform/03): WHEN the
15
39
  * live data was read, WHICH stores answered, and how many records the
@@ -44,8 +68,21 @@ export interface AppliedContext {
44
68
  standard: string;
45
69
  clause?: string;
46
70
  };
71
+ /** the affordance channel's echo (TODO.ai-platform/08): the declared
72
+ * machine's state + how many acts it offered the declaring user's role
73
+ * — the panel's honest context line reads THIS, never its own copy */
74
+ machine?: {
75
+ state: string;
76
+ offered: number;
77
+ };
47
78
  }
48
79
  export declare const NO_CONTEXT: AppliedContext;
80
+ export declare function parseMachine(v: any): MachineContext | undefined;
81
+ /** The proposal bound (TODO.ai-platform/08): a draft act the offered set
82
+ * does not carry is never proposed. An absent facet is not a bound (the
83
+ * page publishes none); a present facet with an empty acts array bounds
84
+ * to NOTHING — the user can read but not act. */
85
+ export declare function machineOffers(machine: MachineContext | undefined, action: string): boolean;
49
86
  /** Parse + bound the ask body's optional `context` field. Anything
50
87
  * malformed degrades to null (no context), never to a 400 — a context
51
88
  * the service can't parse is a context it must not apply. */
package/dist/drafts.d.ts CHANGED
@@ -104,6 +104,11 @@ interface Extraction {
104
104
  source?: string;
105
105
  }>;
106
106
  }
107
+ /** The traceability primitive, shared with the api_call act's body guard
108
+ * (apicalls.ts): a value traces when it appears in the user's own
109
+ * messages (normalized). The proposing model never gets the benefit of
110
+ * the doubt — the user's words are the only source. */
111
+ export declare function valueTracesToUser(value: unknown, userTurns: string[]): boolean;
107
112
  export interface GuardedFields {
108
113
  standard?: string;
109
114
  family_designation?: string;
package/dist/env.d.ts CHANGED
@@ -47,10 +47,12 @@ import type { VectorIndex } from "./ports/vector.ts";
47
47
  import type { Kv } from "./ports/kv.ts";
48
48
  import type { Blobs } from "./ports/blobs.ts";
49
49
  import type { Runtime } from "./ports/runtime.ts";
50
+ import type { StoreQuery } from "./ports/store.ts";
50
51
  export declare function portModelRunner(env: Env): ModelRunner;
51
52
  export declare function portIndex(env: Env, which?: "public" | "primmel" | "composed" | "plain" | "adoc" | "mko" | "pflat" | "glossary"): VectorIndex;
52
53
  export declare function portKv(env: Env): Kv;
53
54
  export declare function portBlobs(env: Env): Blobs;
55
+ export declare function portStore(env: Env): StoreQuery;
54
56
  /** Does the deployment bind this lane index? Presence wiring stays in
55
57
  * the ports layer so domain stages never touch raw bindings. */
56
58
  export declare function hasLane(env: Env, which: "glossary"): boolean;
package/dist/quota.d.ts CHANGED
@@ -13,4 +13,5 @@ export declare function checkQuota(env: Env, bucket: string, id: string, limit:
13
13
  export declare function telemetry(env: Env, ctx: Background, tier: string, route: string, model: string | null, ok: boolean, answerChars: number, queryHash: string, lang?: string, cache?: "exact" | "semantic", meta?: {
14
14
  durationMs?: number;
15
15
  keyId?: string | null;
16
+ retries?: number;
16
17
  }): void;
@@ -32,8 +32,16 @@ export declare function entitlementScope(keys: Set<string>): Set<string> | null;
32
32
  /** Validate + intersect. Returns { error } when the request explicitly
33
33
  * disables every dataset (a user error, not a scope). The corpora a
34
34
  * dataset searches travel WITH the declaration (profile datasets.yaml,
35
- * `corpora:`) — the engine maps no publisher names. */
36
- export declare function resolveRequestScope(body: any, member: unknown): RequestScope | {
35
+ * `corpora:`) — the engine maps no publisher names.
36
+ *
37
+ * `opts.keyWithEntitlements` (the key-tier licensed federation): an
38
+ * API-key caller whose request carries a VALIDATED entitlement set is
39
+ * admitted to the session-gated (federated) datasets — the key's
40
+ * entitlement set is the licensed scope the internal lane honors; the
41
+ * internal worker re-validates the key server-side before it answers. */
42
+ export declare function resolveRequestScope(body: any, member: unknown, opts?: {
43
+ keyWithEntitlements?: boolean;
44
+ }): RequestScope | {
37
45
  error: "empty-datasets";
38
46
  };
39
47
  /** The answer-cache salt: request-scoped context that materially changes
@@ -0,0 +1,12 @@
1
+ import {
2
+ handleSearch
3
+ } from "./chunk-MNNE7OIM.js";
4
+ import "./chunk-DADOI4Q6.js";
5
+ import "./chunk-R2V3X6SQ.js";
6
+ import "./chunk-A3QHHUN5.js";
7
+ import "./chunk-4GJGBGJK.js";
8
+ import "./chunk-V46XM2GU.js";
9
+ import "./chunk-3FYJM7LH.js";
10
+ export {
11
+ handleSearch
12
+ };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  authenticate
3
- } from "../../chunk-2RKYO3OC.js";
4
- import "../../chunk-OMXAE27N.js";
3
+ } from "../../chunk-R2V3X6SQ.js";
4
+ import "../../chunk-V46XM2GU.js";
5
5
  import {
6
6
  P,
7
7
  setProfile
@@ -16,7 +16,7 @@ import {
16
16
  roleModel,
17
17
  sha256Hex,
18
18
  today
19
- } from "../../chunk-OMXAE27N.js";
19
+ } from "../../chunk-V46XM2GU.js";
20
20
  import "../../chunk-3FYJM7LH.js";
21
21
  export {
22
22
  DATASETS,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  handleSearch
3
- } from "../../chunk-S7NLWPNX.js";
3
+ } from "../../chunk-MNNE7OIM.js";
4
4
  import {
5
5
  bindModelNode,
6
6
  checkQuoteAnchors,
@@ -9,7 +9,7 @@ import {
9
9
  modelGroundingBlock,
10
10
  scoreJudge,
11
11
  standardForDocNumber
12
- } from "../../chunk-TDKNQPA2.js";
12
+ } from "../../chunk-MHA2OFWE.js";
13
13
  import {
14
14
  buildMessages,
15
15
  citations,
@@ -32,7 +32,7 @@ import {
32
32
  sessionFrom,
33
33
  telemetry,
34
34
  understandQuery
35
- } from "../../chunk-F66NYFUK.js";
35
+ } from "../../chunk-DADOI4Q6.js";
36
36
  import {
37
37
  authenticate,
38
38
  corsHeaders,
@@ -41,14 +41,14 @@ import {
41
41
  readJson,
42
42
  validateQuery,
43
43
  withCors
44
- } from "../../chunk-2RKYO3OC.js";
44
+ } from "../../chunk-R2V3X6SQ.js";
45
45
  import {
46
46
  canonicalRefusal
47
47
  } from "../../chunk-A3QHHUN5.js";
48
48
  import {
49
49
  entitlementScope,
50
50
  standardKeysFrom
51
- } from "../../chunk-JGKSUSF5.js";
51
+ } from "../../chunk-4GJGBGJK.js";
52
52
  import {
53
53
  LIMITS,
54
54
  MODELS,
@@ -59,7 +59,7 @@ import {
59
59
  roleModel,
60
60
  sha256Hex,
61
61
  today
62
- } from "../../chunk-OMXAE27N.js";
62
+ } from "../../chunk-V46XM2GU.js";
63
63
  import {
64
64
  P,
65
65
  setProfile
@@ -996,7 +996,7 @@ async function handleMcp(env, ctx, req, tier, key) {
996
996
  // stream:false forces the JSON lane (anon defaults to SSE)
997
997
  body: JSON.stringify({ ...args, stream: false })
998
998
  });
999
- const res = name === "ask" ? await (await import("../../ask-43C2WZZO.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-NHTT5DDI.js")).handleSearch(env, ctx, inner, tier, key);
999
+ const res = name === "ask" ? await (await import("../../ask-BDD5TFNC.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-EADF54TY.js")).handleSearch(env, ctx, inner, tier, key);
1000
1000
  return res.json().catch(() => ({ error: { message: "tool transport failed", status: res.status } }));
1001
1001
  });
1002
1002
  if (out.ok && "accepted" in out) return new Response(null, { status: 202 });
@@ -1382,14 +1382,16 @@ async function adminStatsRoute(c) {
1382
1382
  if (!env.ADMIN_TOKEN) return err(501, "admin_disabled", "ADMIN_TOKEN secret is not configured");
1383
1383
  const auth = req.headers.get("authorization") ?? "";
1384
1384
  if (auth !== `Bearer ${env.ADMIN_TOKEN}`) return err(401, "unauthorized", "Invalid admin token");
1385
- const [byDay, byModel, feedback, convCount, cacheMix, durations] = await Promise.all([
1385
+ const [byDay, byModel, feedback, convCount, cacheMix, durations, retriesRow] = await Promise.all([
1386
1386
  env.DB.prepare("SELECT day, tier, COUNT(*) as n, SUM(ok) as ok FROM queries WHERE day >= date('now','-7 days') GROUP BY day, tier ORDER BY day DESC").all(),
1387
1387
  env.DB.prepare("SELECT model, SUM(requests) as requests FROM spend WHERE day >= date('now','-7 days') GROUP BY model ORDER BY requests DESC").all(),
1388
1388
  env.DB.prepare("SELECT rating, COUNT(*) as n FROM feedback GROUP BY rating").all(),
1389
1389
  env.DB.prepare("SELECT COUNT(*) as n FROM conversations").first(),
1390
1390
  env.DB.prepare("SELECT COALESCE(cache, 'miss') AS cache, COUNT(*) AS n FROM queries WHERE day >= date('now','-7 days') AND route = 'ask' GROUP BY cache").all(),
1391
- env.DB.prepare("SELECT duration_ms FROM queries WHERE day >= date('now','-7 days') AND route = 'ask' AND duration_ms IS NOT NULL").all()
1391
+ env.DB.prepare("SELECT duration_ms FROM queries WHERE day >= date('now','-7 days') AND route = 'ask' AND duration_ms IS NOT NULL").all(),
1392
+ env.DB.prepare("SELECT COALESCE(SUM(retries), 0) AS total, SUM(CASE WHEN retries > 0 THEN 1 ELSE 0 END) AS answers FROM queries WHERE day >= date('now','-7 days') AND route = 'ask'").all()
1392
1393
  ]);
1394
+ const retries = retriesRow?.results?.[0] ?? { total: 0, answers: 0 };
1393
1395
  const ds = durations.results.map((r) => r.duration_ms).sort((a, b) => a - b);
1394
1396
  const pct = (q) => ds.length ? ds[Math.min(ds.length - 1, Math.floor(q * ds.length))] : null;
1395
1397
  const totalQueries = byDay.results.reduce((a, r) => a + (r.n || 0), 0) || 0;
@@ -1407,6 +1409,7 @@ async function adminStatsRoute(c) {
1407
1409
  feedback: feedback.results,
1408
1410
  cache_mix_7d: cacheMix.results,
1409
1411
  latency_ms: ds.length ? { n: ds.length, p50: pct(0.5), p95: pct(0.95) } : null,
1412
+ generate_retries_7d: { total: Number(retries.total) || 0, answers_retried: Number(retries.answers) || 0 },
1410
1413
  conversations: convCount?.n ?? 0,
1411
1414
  error_rate_pct: errorRate,
1412
1415
  index_version: env.INDEX_VERSION,
@@ -4,8 +4,8 @@ import {
4
4
  requestSalt,
5
5
  resolveRequestScope,
6
6
  standardKeysFrom
7
- } from "../../chunk-JGKSUSF5.js";
8
- import "../../chunk-OMXAE27N.js";
7
+ } from "../../chunk-4GJGBGJK.js";
8
+ import "../../chunk-V46XM2GU.js";
9
9
  import "../../chunk-3FYJM7LH.js";
10
10
  export {
11
11
  entitlementScope,
package/docs/API.md CHANGED
@@ -117,6 +117,43 @@ a corpus document. The service applies it honestly:
117
117
  declaration, not just the query) and is never written into them.
118
118
  - Conversational turns (greetings, identity) never ground in a declared
119
119
  context — the echo reports `none`.
120
+ - The entity declaration may carry the affordance channel's `machine`
121
+ facet (TODO.ai-platform/08): the lifecycle machine's current `state`
122
+ (the fact) and the `acts` it offers there for the declaring user's
123
+ role (already role-filtered by the platform). The facet parses bounded
124
+ (state ≤ 60 chars, ≤ 24 acts, each `action`/`to`/`guard` ≤ 60), rides
125
+ the entity kind only, and BOUNDS the proposal grammar: a draft act the
126
+ set does not offer is never proposed, and an empty `acts` array is the
127
+ honest "the user can read but not act" — the answer says so and no
128
+ draft rides. The facet is advisory; the platform re-validates every
129
+ draft act authoritatively on the way back.
130
+
131
+ ```jsonc
132
+ "context": {
133
+ "kind": "entity",
134
+ "label": "this application R60-2026-0042",
135
+ "route": "/app/ia/applications/R60-2026-0042",
136
+ "machine": { // the affordance channel — entity kind only
137
+ "state": "UNDER_REVIEW", // the machine's current state (the fact)
138
+ "acts": [ // offered FOR THE DECLARING USER'S ROLE — may be []
139
+ { "action": "ia_accepts", "to": "ACCEPTED" },
140
+ { "action": "ia_logs_samples", "to": "SAMPLES_RECEIVED", "guard": "receipt_complete" }
141
+ ]
142
+ }
143
+ }
144
+ ```
145
+
146
+ The echo carries the facet's fact + count, so the panel's honest context
147
+ line reads the service's applied truth, never its own copy:
148
+
149
+ ```jsonc
150
+ "context_applied": {
151
+ "kind": "entity",
152
+ "label": "this application R60-2026-0042",
153
+ "scoped_to": "OIML R 60:2021",
154
+ "machine": { "state": "UNDER_REVIEW", "offered": 2 }
155
+ }
156
+ ```
120
157
 
121
158
  Every ask response — the SSE `citations`/`done` events and the JSON body —
122
159
  echoes what was APPLIED, so the panel's context line never invents a
@@ -245,6 +282,52 @@ The honest rules, all eval-gated (the golden suite's `draft-*` legs):
245
282
 
246
283
  **SSE**: the `draft` rides the first (`citations`) frame beside
247
284
  `context_applied`; `token` frames carry the answer; `done` as usual.
285
+
286
+ ##### The api_call act (TODO.ai-platform/09 — the operations deployment)
287
+
288
+ The second draft act proposes ONE platform operation — the operations
289
+ assistant's grammar, feature-flagged (`features.api_call_drafts`; the
290
+ corpus-only deployment leaves it off):
291
+
292
+ ```jsonc
293
+ "draft": {
294
+ "kind": "draft",
295
+ "act": "api_call",
296
+ "version": 1,
297
+ "title": "Save the R 60 filter", // what the act DOES — never the wire shape
298
+ "prepared_at": "2026-09-26T09:00:00Z",
299
+ "requires_confirmation": true, // ALWAYS
300
+ "call": {
301
+ "method": "PUT", // a real operation from the plane —
302
+ "path": "/api/notify/preferences", // an invented path is a service bug
303
+ "body": { } // optional; preference bodies trace
304
+ } // to the user's own words
305
+ }
306
+ ```
307
+
308
+ The honest rules (the module is structurally write-free — it reads the
309
+ operation directory and calls the pick model; there is no platform IO):
310
+
311
+ - **The directory is the closed world**: the call names an operation the
312
+ deployment's `api_ops` directory (loaded from the OpenAPI plane at
313
+ ingest) actually carries. The model proposes an operation id; the
314
+ guard resolves it against the directory AFTER the fact — an invented
315
+ id or path never rides.
316
+ - **The machine facet bounds the machine acts** (§2.1.1): the entity
317
+ write composes only for an act the offered set carries, addressed to
318
+ the declared entity (the profile's route-pattern table maps the
319
+ page's route to the store and id). An empty offered set answers the
320
+ honest "you can read but not act" and proposes nothing.
321
+ - **The never-offer list holds** (profile data, `publisher.acts
322
+ .never_offer`): the auth family (signout is the user's own, forever)
323
+ and the grant's own mint/revoke (delegation is granted by the user's
324
+ hand on the settings page, never by a chat message) are excluded from
325
+ the pick prompt AND refused at the guard.
326
+ - **The act class is the platform's word, not the draft's**: the panel
327
+ pre-flights every api_call draft against the plane and the class
328
+ decides the arc — preference rides the standing grant's seam, record
329
+ confirms per act and executes as the user.
330
+
248
331
  The draft is ephemeral — the conversations API never persists it (a
249
332
  resumed session keeps the honest context line, not a stale draft).
250
333
 
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.32",
30
+ "version": "0.2.34",
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",
@@ -0,0 +1,4 @@
1
+ -- the latency program's aggregate: how many generation retries each ask
2
+ -- needed (Server-Timing carried the count per answer; this column makes
3
+ -- it rollup-able). Existing rows read 0 — they predate the column.
4
+ ALTER TABLE queries ADD COLUMN retries INTEGER NOT NULL DEFAULT 0;