fix(plugins): report a plugin SQL call done once its last statement has run

The time budget for an exec script was read after every statement, the
last one included. A single statement without args, or a script whose
closing COMMIT crossed the budget, had already run and committed, yet the
call threw a time budget error. A plugin that retries on error then wrote
the same rows a second time.

The script now reads the clock before each statement after the first, so
it still stops before the next statement once the time is up and rolls
back a transaction it opened, but a script whose statements have all run
is reported as done. query() likewise reads the clock only once another
row has come back, so a result read to its end is returned.

The ctx.db doc in the SDK said a single statement was not timed, which
contradicted the query budget. It now names what is really untimed: an
exec with bound args and migrate. README and wiki describe the script
stopping before its next statement.
This commit is contained in:
Maurice
2026-10-08 18:11:34 +02:00
parent d3be6fb2b8
commit 465480630b
5 changed files with 79 additions and 23 deletions
+1 -1
View File
@@ -252,7 +252,7 @@ for a legitimate plugin and only bite a runaway or abusive one — build against
| `ctx.ai` | 200 calls/day per plugin (UTC midnight rollover); past it throws `"daily AI budget exhausted (resets at UTC midnight)"` |
| `ctx.notify` | 100 calls/day per plugin (UTC midnight rollover); past it throws `"daily notification budget exhausted (resets at UTC midnight)"` |
| RPC (every `ctx.*` call) | burst 60, sustained 20/s, 16 in-flight per plugin; a throttled call is refused with `HOST_ERROR: rate limit exceeded — slow down ctx.* calls` |
| `ctx.db` (your own sqlite) | 256 MB per plugin; a `query` or `tx` returns at most 100,000 rows (a whole `tx` batch shares one count). A `query`, a whole `tx` batch and an `exec` script without bound args each have 2 s of wall-clock time, checked between the rows a statement reads and between the statements of a batch or script; past either limit the call throws (`query exceeded its 2000 ms time budget`), a `tx` rolls back, and an `exec` script stops after the statement that ran out of time (the statements before it stay applied, as they always did, and a transaction the script opened itself is rolled back). Split a longer script over several calls, or batch it with `tx`. `migrate` has no time budget, since a migration stopped part of the way would fail again on every start. `ATTACH`, `DETACH`, `VACUUM`, `PRAGMA`, `WITH RECURSIVE` and `load_extension` are refused. The budget cannot stop a single statement, because SQLite runs it on TREK's main thread in one step: a write runs to its end (an `INSERT ... SELECT` over a cross join of large tables), and a read runs until its first row (an aggregate over one). Keep your statements indexed and bounded |
| `ctx.db` (your own sqlite) | 256 MB per plugin; a `query` or `tx` returns at most 100,000 rows (a whole `tx` batch shares one count). A `query`, a whole `tx` batch and an `exec` script without bound args each have 2 s of wall-clock time, checked between the rows a statement reads and between the statements of a batch or script; past either limit the call throws (`query exceeded its 2000 ms time budget`), a `tx` rolls back, and an `exec` script stops before its next statement once the time is up (the statements that ran stay applied, as they always did, and a transaction the script opened itself is rolled back; a script whose last statement has run counts as done, however long it took). Split a longer script over several calls, or batch it with `tx`. `migrate` has no time budget, since a migration stopped part of the way would fail again on every start. `ATTACH`, `DETACH`, `VACUUM`, `PRAGMA`, `WITH RECURSIVE` and `load_extension` are refused. The budget cannot stop a single statement, because SQLite runs it on TREK's main thread in one step: a write runs to its end (an `INSERT ... SELECT` over a cross join of large tables), and a read runs until its first row (an aggregate over one). Keep your statements indexed and bounded |
| `ctx.meta` | 64 KB per value, 256 chars per key, 100 keys per (plugin, entity) |
| Plugin process | 300 MB RSS ceiling; auto-disabled after 5 crashes in 5 minutes |
| Event redelivery buffer | 200 events held per plugin, dropped unreplayed after 15 minutes |
+4 -2
View File
@@ -74,8 +74,10 @@ export interface PluginContext {
* path or connection to directly. Quota: 256 MB per plugin (writes past it fail);
* result sets are capped at 100,000 rows, and a `query`, a `tx` or an `exec` script
* without args gets 2 s of wall-clock time, checked between the rows it reads and
* the statements it runs (past it, it throws, a `tx` rolls back and a script stops).
* A single statement and `migrate` are not timed.
* the statements it runs (past it, it throws, a `tx` rolls back and a script stops
* before its next statement). An `exec` with bound args (one statement) and
* `migrate` are not timed, and no single statement can be stopped midway (a read
* only between its rows).
* See `tx()` below for the atomic-batch cap and README § Runtime limits. */
db: {
query<T = unknown>(sql: string, ...args: unknown[]): Promise<T[]>;