@vibes.diy/prompts 12.1.1 → 12.1.2

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 (2) hide show
  1. package/llms/backend.md +77 -22
  2. package/package.json +4 -4
package/llms/backend.md CHANGED
@@ -127,7 +127,8 @@ ctx.userInfo; // { userHandle } or null — who the handler is acting as
127
127
  ctx.secrets; // Record<string,string> — the owner's per-vibe secrets; frozen, {} when none
128
128
  await ctx.db.put(doc, { db: "notes", id: "optional-id" }); // resolves to the doc id AFTER commit
129
129
  await ctx.db.delete(docId, { db: "notes" });
130
- const docs = await ctx.db.query({ db: "notes" }); // latest non-deleted docs (each with _id)
130
+ const docs = await ctx.db.query({ db: "notes", field: "type", key: "note", limit: 100 }); // a filtered page
131
+ const doc = await ctx.db.get("state:cursor", { db: "notes" }); // one doc by _id, or null
131
132
  const text = await ctx.callAI("prompt", { model: "openrouter/auto", max_tokens: 500 }); // server-side AI call
132
133
  const res = await ctx.fetch("https://api.example.com/x", { headers }); // outbound HTTP — use this, not bare fetch()
133
134
  ```
@@ -137,13 +138,52 @@ const res = await ctx.fetch("https://api.example.com/x", { headers }); // outbou
137
138
  database that triggered the event is the default.
138
139
  - Always `await` db calls; they resolve only after the write commits (or throw
139
140
  when the access function denies it).
140
- - `ctx.db.query({ db })` returns the whole database's latest docs (capped at
141
- 2000) filter and sort in the handler. It is read-ACL-gated as the acting
142
- identity and anonymous `fetch` callers are denied. In `scheduled` (admin
143
- mode) the read is **unfiltered** access-fn-bound databases return every
144
- doc, across all users and channels. In `fetch`/`onChange`, databases bound
145
- to an access function cannot be queried (keep those lanes' read databases on
146
- plain ACLs). Made for `scheduled` sweeps: read, decide, then `put`/`delete`.
141
+ - `ctx.db.get(id, { db })` resolves the single document with that `_id`, or
142
+ `null` when it is missing or deleted. It is the right read whenever you can
143
+ name the document a tick's state doc, a token lookup, a singleton. Never
144
+ scan a database looking for a doc whose id you already know.
145
+ - `ctx.db.query({ db, field, key, keys, range, prefix, limit, after })` reads a
146
+ **page** of the database's latest docs. Every filter is optional and they
147
+ combine: `field` names the document field to match on, then `key` (one exact
148
+ value), `keys` (any of several), `range` (`[from, to]`) or `prefix` (a string,
149
+ or an array prefix — charwise-encoded, same semantics the client's queries
150
+ use) narrows it. Three edges to get right: **no `field` means no filtering**
151
+ (you get a raw page of everything), a bare `field` with no selector still
152
+ drops docs that lack that field, and `prefix` takes the **raw** value — a
153
+ plain string or array, never anything pre-encoded (the encoding is internal).
154
+ `limit` only clamps the page **down**; the 2000-doc cap stays. Ask for what
155
+ the handler actually needs — reading the whole database and filtering in the
156
+ handler wastes the cap and goes blind as the app grows. Sorting still happens
157
+ in the handler.
158
+ - The result is a real `Array` of docs that **also** destructures — both
159
+ `const docs = await ctx.db.query(...)` and
160
+ `const { docs, next } = await ctx.db.query(...)` work.
161
+ - **Paginate on `next`, never on emptiness.** `next` is computed from the raw
162
+ page *before* filtering, and only when that page came back full — so a
163
+ narrowly filtered page can be **empty and still carry `next`**. Stopping at
164
+ the first empty page silently truncates the read:
165
+
166
+ ```js
167
+ let after;
168
+ do {
169
+ const page = await ctx.db.query({ db: "faves", field: "type", key: "favorite", limit: 500, after });
170
+ for (const doc of page) {
171
+ // …
172
+ }
173
+ after = page.next;
174
+ } while (after);
175
+ ```
176
+
177
+ `next` rides on the array itself, so read it **before** transforming: any
178
+ array operation (`.filter()`, `.map()`, spread) returns a plain array with no
179
+ `next`. On the last page the property is absent, not undefined — `"next" in
180
+ page` is `false`.
181
+ - Both reads are read-ACL-gated as the acting identity, through the same gate:
182
+ anonymous `fetch` callers are denied, and in `fetch`/`onChange` a database
183
+ bound to an access function cannot be read at all (keep those lanes' read
184
+ databases on plain ACLs). In `scheduled` (admin mode) reads are
185
+ **unfiltered** — access-fn-bound databases return every doc, across all users
186
+ and channels. Made for `scheduled` sweeps: read, decide, then `put`/`delete`.
147
187
  - The one exception to that last sentence is the declared opt-in above,
148
188
  `config.fetch.unfilteredReads` — a `fetch` handler that names a database there
149
189
  reads it unfiltered, anonymous callers included, and owes the authorization
@@ -220,8 +260,8 @@ const SNAPSHOT = [
220
260
  ];
221
261
 
222
262
  async function ensureSnapshot(ctx) {
223
- const docs = await ctx.db.query({ db: "nodeReleases" });
224
- if (docs.some((doc) => doc.type === "release")) return;
263
+ // Point-read by id the snapshot has a known `_id`, so there's nothing to scan for.
264
+ if (await ctx.db.get(SNAPSHOT[0]._id, { db: "nodeReleases" })) return;
225
265
  for (const doc of SNAPSHOT) await ctx.db.put(doc, { db: "nodeReleases" });
226
266
  }
227
267
 
@@ -233,18 +273,19 @@ function unchanged(existing, next) {
233
273
  return Object.keys(next).every((k) => existing[k] === next[k]);
234
274
  }
235
275
 
236
- async function putIfChanged(ctx, byId, next) {
237
- if (unchanged(byId.get(next._id), next)) return;
276
+ // The diff source is a POINT-READ per incoming id, not a query over the stored
277
+ // set: the work is bounded by what the feed sent (≤20 docs), never by how big
278
+ // the database has grown — so a db that outgrows a query page can never
279
+ // silently eject rows from the diff and turn this tick into a rewrite loop.
280
+ async function putIfChanged(ctx, next) {
281
+ const existing = await ctx.db.get(next._id, { db: "nodeReleases" });
282
+ if (unchanged(existing, next)) return;
238
283
  await ctx.db.put(next, { db: "nodeReleases" });
239
284
  }
240
285
 
241
286
  export async function scheduled(event, ctx) {
242
287
  await ensureSnapshot(ctx);
243
288
 
244
- // Read current docs once so each write can diff against what's already stored.
245
- const current = await ctx.db.query({ db: "nodeReleases" });
246
- const byId = new Map(current.map((doc) => [doc._id, doc]));
247
-
248
289
  // One request, no retry storm; ctx.fetch enforces 15s/10MB and egress caps.
249
290
  const response = await ctx.fetch("https://api.github.com/repos/nodejs/node/releases?per_page=20", {
250
291
  headers: { accept: "application/vnd.github+json" },
@@ -258,7 +299,7 @@ export async function scheduled(event, ctx) {
258
299
  if (denied?.vibesEgressDenied === true) {
259
300
  // A real state change — putIfChanged writes once on entering denial, then
260
301
  // nothing while it persists. No per-tick timestamp, so no churn.
261
- await putIfChanged(ctx, byId, {
302
+ await putIfChanged(ctx, {
262
303
  _id: "refresh:status",
263
304
  type: "refreshStatus",
264
305
  state: "egress-denied",
@@ -276,7 +317,7 @@ export async function scheduled(event, ctx) {
276
317
  for (const row of rows.slice(0, 20)) {
277
318
  if (!row?.id || !row?.html_url || !row?.published_at) continue;
278
319
  // Stable per-item `_id` + diff: a row whose content is identical is skipped.
279
- await putIfChanged(ctx, byId, {
320
+ await putIfChanged(ctx, {
280
321
  _id: `release:github-${row.id}`,
281
322
  type: "release",
282
323
  title: String(row.name || row.tag_name || "Node.js release"),
@@ -286,7 +327,7 @@ export async function scheduled(event, ctx) {
286
327
  });
287
328
  }
288
329
 
289
- await putIfChanged(ctx, byId, {
330
+ await putIfChanged(ctx, {
290
331
  _id: "refresh:status",
291
332
  type: "refreshStatus",
292
333
  state: "fresh",
@@ -337,15 +378,29 @@ down. Catch that exact 403 shape and show the saved snapshot with honest copy
337
378
  render a broken screen, or silently label saved data as fresh.
338
379
 
339
380
  **Zero-churn discipline for scheduled ticks.** A cron runs forever, so it must
340
- not write forever. Read the current docs first (`ctx.db.query`), give every item
341
- a stable `_id`, and `put` **only** the rows whose content actually changed — the
342
- `putIfChanged` diff above. Never stamp a per-tick `checkedAt`/`updatedAt`/run
381
+ not write forever. Give every item a stable `_id` and `put` **only** the rows
382
+ whose content actually changed — the `putIfChanged` diff above, which
383
+ point-reads each id it is about to write. Diff against **point-reads of the
384
+ incoming ids**, so the work is bounded by what arrived this tick, not by how
385
+ big the database has grown; reach for a keyed `ctx.db.query` only when the
386
+ stored set itself is the target (and then walk `next` to exhaustion before
387
+ diffing — a one-page diff source re-puts everything the page missed, forever). Never stamp a per-tick `checkedAt`/`updatedAt`/run
343
388
  counter onto a doc: it makes every put look new, and each spurious put mints a
344
389
  fresh revision and a sync-feed row that re-tickles every replica on every tick.
345
390
  A failed or empty upstream fetch aborts with no writes, preserving the last-good
346
391
  rows. Only a genuine content change — a new or updated row, a real state
347
392
  transition — earns a write.
348
393
 
394
+ **A tick's own memory is a document it reads by id.** Whatever a scheduled
395
+ handler needs to know about what it already did — a cursor, a last-synced
396
+ marker, a per-run state — lives in one doc with a fixed `_id`, read with
397
+ `ctx.db.get(id, { db })`. Never re-derive that answer by scanning the database:
398
+ a sweep that diffs against its own whole-db read goes blind the moment the
399
+ database outgrows one page, and then rewrites everything on every tick, forever.
400
+ Keep a copy of the state in module scope as well, and read a **missing or failed**
401
+ state read as *"don't know — skip this tick"*, never as *"nothing has been done
402
+ yet"*: that second reading is exactly what restarts the endless rewrite.
403
+
349
404
  ## fetch — the app's HTTP endpoint
350
405
 
351
406
  Runs for requests to the app's `/_api` route. The request path is rooted after
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibes.diy/prompts",
3
- "version": "12.1.1",
3
+ "version": "12.1.2",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "description": "",
@@ -24,9 +24,9 @@
24
24
  "license": "Apache-2.0",
25
25
  "dependencies": {
26
26
  "@adviser/cement": "~0.5.34",
27
- "@vibes.diy/call-ai-v2": "^12.1.1",
28
- "@vibes.diy/identity": "^12.1.1",
29
- "@vibes.diy/use-vibes-types": "^12.1.1",
27
+ "@vibes.diy/call-ai-v2": "^12.1.2",
28
+ "@vibes.diy/identity": "^12.1.2",
29
+ "@vibes.diy/use-vibes-types": "^12.1.2",
30
30
  "arktype": "~2.2.3",
31
31
  "json-schema-faker": "~0.6.2"
32
32
  },