@aria-framework/ai 0.12.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -207,6 +207,11 @@ module.exports = {
207
207
  // Speed history. Lazy for the same reason as the others: it needs the db-worker driver contract,
208
208
  // which is an optional peer.
209
209
  get createSpeedStore() { return require('./speedStore').createSpeedStore; },
210
+ // EXPORTED SO A CONSUMER CAN ASSERT ITS TABLE MATCHES. An app writes its own migration, which is
211
+ // a hand copy of this DDL — and a copy with nothing comparing it to the original is the failure
212
+ // mode this repo has already documented twice. providerSchemaFor and usageSchemaFor exist for the
213
+ // same reason; leaving this one out meant a drift would surface as an INSERT throwing at runtime.
214
+ get speedSchemaFor() { return require('./speedStore').schemaFor; },
210
215
  // No database behind health, so it loads eagerly like the rest of the seam.
211
216
  ...require('./health'),
212
217
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@aria-framework/ai",
3
3
  "description": "Aria App Framework — AI module. A dependency-injected model seam (createAiClient) over several providers (LM Studio / OpenAI-compatible / Anthropic), with a fact-preservation guard, generic Polish and Generate writing engines, and a browser polish widget. Prompts and config stay in the consuming app.",
4
- "version": "0.12.0",
4
+ "version": "0.12.1",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
7
7
  "publishConfig": {
package/speedStore.js CHANGED
@@ -33,6 +33,22 @@ function createSpeedStore(opts = {}) {
33
33
 
34
34
  const num = (v) => (v == null || v === '' || Number.isNaN(Number(v)) ? null : Number(v));
35
35
 
36
+ /**
37
+ * `YYYY-MM-DD HH:MM:SS` in UTC — what `datetime('now')` produces, and what every other timestamp
38
+ * column in a consuming app already holds.
39
+ *
40
+ * toISOString() was the obvious thing to write and the wrong thing to store. Apps parse these
41
+ * columns with `new Date(String(ts).replace(' ', 'T') + 'Z')`; given an ISO string there is no
42
+ * space to replace, so the 'Z' is appended to one already there and the result is Invalid Date —
43
+ * silently, because the helper checks isNaN and returns ''. Sorting is worse: 'T' (0x54) sorts
44
+ * after ' ' (0x20), so an ISO row and a datetime('now') row from the same second compare wrong.
45
+ */
46
+ const stamp = (d) => {
47
+ const p2 = (n) => String(n).padStart(2, '0');
48
+ return `${d.getUTCFullYear()}-${p2(d.getUTCMonth() + 1)}-${p2(d.getUTCDate())} `
49
+ + `${p2(d.getUTCHours())}:${p2(d.getUTCMinutes())}:${p2(d.getUTCSeconds())}`;
50
+ };
51
+
36
52
  return {
37
53
  table,
38
54
 
@@ -50,7 +66,7 @@ function createSpeedStore(opts = {}) {
50
66
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
51
67
  [
52
68
  String(entry.endpoint || ''),
53
- (entry.at || now()).toISOString(),
69
+ stamp(entry.at || now()),
54
70
  num(entry.tokensPerSec),
55
71
  num(entry.coldMs),
56
72
  num(entry.runTokens),
@@ -80,15 +96,29 @@ function createSpeedStore(opts = {}) {
80
96
  const ids = (endpoints || []).map((e) => String(e)).filter(Boolean);
81
97
  if (!ids.length) return {};
82
98
  const capped = Math.max(1, Math.min(500, Number(limit) || DEFAULT_LIMIT));
83
- const rows = await driver.all(
84
- `SELECT * FROM ${table} WHERE endpoint IN (${ids.map(() => '?').join(', ')}) ORDER BY id DESC`,
85
- ids
86
- );
99
+ // LIMITED PER ENDPOINT, IN SQL. A single `WHERE endpoint IN (...) ORDER BY id DESC` reads
100
+ // every retained row for every endpoint and throws most of them away in JS — at the 200-row
101
+ // bound with eight endpoints that is 1600 rows, `error` text included, marshalled across the
102
+ // worker boundary to draw eight 88-pixel lines. It also cannot use the (endpoint, id DESC)
103
+ // index for a global ordering, so it sorts in a temp b-tree as well.
104
+ //
105
+ // UNION ALL of per-endpoint selects rather than a window function: it reads the same on both
106
+ // engines and needs no version floor.
107
+ // EACH BRANCH IN A SUBQUERY, and aliased. A bare `... ORDER BY id DESC LIMIT n UNION ALL ...`
108
+ // is a syntax error — the ORDER BY binds to the compound, not the branch — and Postgres
109
+ // additionally requires a name for a subquery in FROM. Both engines accept this form.
110
+ const sql = ids
111
+ .map((_, i) => `SELECT * FROM (SELECT * FROM ${table} WHERE endpoint = ? `
112
+ + `ORDER BY id DESC LIMIT ${capped}) AS s${i}`)
113
+ .join(' UNION ALL ');
114
+ const rows = await driver.all(sql, ids);
87
115
  const out = {};
88
116
  for (const row of rows) {
89
117
  const bucket = out[row.endpoint] || (out[row.endpoint] = { latest: null, points: [] });
90
- if (!bucket.latest) bucket.latest = row; // rows arrive newest first
91
- if (bucket.points.length < capped) bucket.points.push(row);
118
+ // Newest first WITHIN each endpoint's block, which is all this needs — the blocks
119
+ // themselves may arrive in any order.
120
+ if (!bucket.latest) bucket.latest = row;
121
+ bucket.points.push(row);
92
122
  }
93
123
  // Reversed at the end so a caller draws left-to-right in time without thinking about it.
94
124
  for (const id of Object.keys(out)) out[id].points.reverse();
@@ -102,9 +132,15 @@ function createSpeedStore(opts = {}) {
102
132
  * history, and one checked hourly should not be able to bury it. Age-based pruning gets that
103
133
  * backwards for exactly the endpoints whose trend matters most.
104
134
  */
105
- async prune({ keepPerEndpoint = 200 } = {}) {
135
+ async prune({ keepPerEndpoint = 200, endpoint = null } = {}) {
106
136
  const keep = Math.max(10, Math.min(5000, Number(keepPerEndpoint) || 200));
107
- const ids = await driver.all(`SELECT DISTINCT endpoint FROM ${table}`, []);
137
+ // ONE ENDPOINT WHEN THE CALLER KNOWS WHICH. Pruning on write swept every endpoint in the
138
+ // table every time — 1 + 2N queries to delete at most one row belonging to one of them, since
139
+ // none of the others can have grown since their own last write. With eight endpoints that was
140
+ // seventeen worker round trips inside a request, for one deletion.
141
+ const ids = endpoint
142
+ ? [{ endpoint: String(endpoint) }]
143
+ : await driver.all(`SELECT DISTINCT endpoint FROM ${table}`, []);
108
144
  let removed = 0;
109
145
  for (const { endpoint } of ids) {
110
146
  const cutoff = await driver.get(