@plurnk/plurnk-service 0.55.0 → 0.56.0

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 (77) hide show
  1. package/.env.example +0 -8
  2. package/SPEC.md +111 -93
  3. package/dist/content/matcher.d.ts.map +1 -1
  4. package/dist/content/matcher.js +62 -89
  5. package/dist/content/matcher.js.map +1 -1
  6. package/dist/core/ChannelWrite.d.ts +2 -1
  7. package/dist/core/ChannelWrite.d.ts.map +1 -1
  8. package/dist/core/ChannelWrite.js +2 -2
  9. package/dist/core/ChannelWrite.js.map +1 -1
  10. package/dist/core/ChannelWrite.sql +2 -2
  11. package/dist/core/Engine.d.ts +1 -0
  12. package/dist/core/Engine.d.ts.map +1 -1
  13. package/dist/core/Engine.js +115 -50
  14. package/dist/core/Engine.js.map +1 -1
  15. package/dist/core/Engine.sql +40 -2
  16. package/dist/core/fork.d.ts.map +1 -1
  17. package/dist/core/fork.js +20 -4
  18. package/dist/core/fork.js.map +1 -1
  19. package/dist/core/fork.sql +29 -0
  20. package/dist/core/packet-inject.d.ts +2 -0
  21. package/dist/core/packet-inject.d.ts.map +1 -1
  22. package/dist/core/packet-inject.js +28 -1
  23. package/dist/core/packet-inject.js.map +1 -1
  24. package/dist/core/packet-wire.d.ts.map +1 -1
  25. package/dist/core/packet-wire.js +22 -2
  26. package/dist/core/packet-wire.js.map +1 -1
  27. package/dist/core/session-settings.d.ts +0 -5
  28. package/dist/core/session-settings.d.ts.map +1 -1
  29. package/dist/core/session-settings.js +1 -10
  30. package/dist/core/session-settings.js.map +1 -1
  31. package/dist/schemes/Exec.d.ts.map +1 -1
  32. package/dist/schemes/Exec.js +32 -6
  33. package/dist/schemes/Exec.js.map +1 -1
  34. package/dist/schemes/File.d.ts.map +1 -1
  35. package/dist/schemes/File.js +5 -1
  36. package/dist/schemes/File.js.map +1 -1
  37. package/dist/schemes/Log.d.ts +4 -0
  38. package/dist/schemes/Log.d.ts.map +1 -1
  39. package/dist/schemes/Log.js +35 -19
  40. package/dist/schemes/Log.js.map +1 -1
  41. package/dist/schemes/Log.sql +14 -11
  42. package/dist/schemes/Run.d.ts +3 -1
  43. package/dist/schemes/Run.d.ts.map +1 -1
  44. package/dist/schemes/Run.js +16 -0
  45. package/dist/schemes/Run.js.map +1 -1
  46. package/dist/schemes/_entry-find.d.ts.map +1 -1
  47. package/dist/schemes/_entry-find.js +17 -3
  48. package/dist/schemes/_entry-find.js.map +1 -1
  49. package/dist/schemes/_entry-find.sql +23 -0
  50. package/dist/schemes/_entry-manifest.d.ts +1 -1
  51. package/dist/schemes/_entry-manifest.d.ts.map +1 -1
  52. package/dist/schemes/_entry-manifest.js +22 -4
  53. package/dist/schemes/_entry-manifest.js.map +1 -1
  54. package/dist/schemes/exec-abort.d.ts +4 -0
  55. package/dist/schemes/exec-abort.d.ts.map +1 -1
  56. package/dist/schemes/exec-abort.js +6 -0
  57. package/dist/schemes/exec-abort.js.map +1 -1
  58. package/dist/server/Daemon.d.ts.map +1 -1
  59. package/dist/server/Daemon.js +128 -19
  60. package/dist/server/Daemon.js.map +1 -1
  61. package/dist/server/MethodRegistry.d.ts +1 -0
  62. package/dist/server/MethodRegistry.d.ts.map +1 -1
  63. package/dist/server/drain.sql +17 -5
  64. package/dist/server/envelope.d.ts.map +1 -1
  65. package/dist/server/envelope.js +0 -9
  66. package/dist/server/envelope.js.map +1 -1
  67. package/dist/server/methods/log_read.d.ts.map +1 -1
  68. package/dist/server/methods/log_read.js +7 -1
  69. package/dist/server/methods/log_read.js.map +1 -1
  70. package/dist/server/methods/log_read.sql +17 -7
  71. package/dist/server/methods/session_create.d.ts.map +1 -1
  72. package/dist/server/methods/session_create.js +1 -8
  73. package/dist/server/methods/session_create.js.map +1 -1
  74. package/docs/run.md +3 -1
  75. package/migrations/0000-00-00.01_schema.sql +11 -1
  76. package/package.json +9 -9
  77. package/requirements.md +4 -0
package/SPEC.md CHANGED
@@ -14,14 +14,14 @@ Canonical meanings. When a doc, comment, test name, or commit message uses one o
14
14
 
15
15
  | Term | Meaning |
16
16
  |---|---|
17
- | **agent** | The plurnk runtime singleton. Owns agent-scoped state (default scheme registry, agent-wide entries). One per process. |
17
+ | **agent** | The plurnk runtime. Acts in-session as the reserved `plurnk` run (§actor-boundary self-hosting), never a privileged singleton owning its own entries — the old *agent scope* is retired (entry scope is now `session` / `run`, §machine-processes). |
18
18
  | **session** | Durable user-named workspace. Persists across runs and process restarts. Identity: `sessions.id` + unique `sessions.name`. |
19
19
  | **run** | A stretch of work within a session. Multiple runs per session. May fork from another run via `parent_run_id`. Owns the log entries. |
20
- | **loop** | One model-driven or client-driven iteration within a run. Status ∈ {100 pending · 102 running · 200 done · 413 budget-overflow · 429 turn-ceiling · 499 cancelled · 500 failed · 508 runaway}. Many loops per run. The model runs inside a loop; each client RPC has its own loop. |
20
+ | **loop** | One model-driven or client-driven iteration within a run. Status ∈ {100 pending · 102 running · 200 done · 202 parked (resumable, §send) · 413 budget-overflow · 429 turn-ceiling · 499 cancelled · 500 failed · 508 runaway}. Many loops per run. The model runs inside a loop; each client RPC has its own loop. |
21
21
  | **turn** | One round-trip with the LLM (or one client RPC dispatch). One assembled prompt sent, one parsed response handled. Many turns per loop. Identity: `(loop_id, sequence)`. |
22
22
  | **op** | One DSL operation the model emits. Parsed into a `PlurnkStatement`. Examples: `EDIT`, `READ`, `SEND`, `FIND`, `COPY`, `MOVE`, `OPEN`, `FOLD`, `EXEC`. One turn produces zero or more ops. |
23
23
  | **statement** | Synonym for parsed op. The AST shape `PlurnkStatement` from `@plurnk/plurnk-grammar`. |
24
- | **action** | One executed op. Action and op are the same thing in different states (op = parsed; action = executed). The execution produces a log_entries row at `log:///<L>/<T>/<S>/<op>`. |
24
+ | **action** | One executed op. Action and op are the same thing in different states (op = parsed; action = executed). The execution produces a log_entries row at `log:///<L>/<T>/<S>/<op>`. (The log also holds an *actionless* `op='error'` row — a model emission that failed to parse, §telemetry — so a failure is curatable like any row.) |
25
25
  | **dispatch** | The engine routing a statement to its scheme's op handler. |
26
26
 
27
27
  ### §storage-terms Storage terms
@@ -30,7 +30,7 @@ Canonical meanings. When a doc, comment, test name, or commit message uses one o
30
30
  |---|---|
31
31
  | **entry** | The unit of canonical state. Identity: `(scope, scheme, pathname)`. Holds one or more `channels` of content plus `tags` and `attributes`. |
32
32
  | **channel** | A named content buffer on an entry. Examples: `body`, `stdout`, `stderr`, `headers`, `symbols`. Each channel has `content`, `mimetype`, `tokens`, `state`. |
33
- | **scope** | `"agent"` or `"session"`. Determines who reads. Agent-scope entries visible to every run; session-scope entries to that session's runs. |
33
+ | **scope** | `"session"` or `"run"`. Determines who reads: session-scope entries are the shared world (every run in the session), run-scope entries are a run's private scratch (§machine-processes). *Agent scope is retired.* |
34
34
  | **scheme** | A URI prefix + handler. `known`, `unknown`, `file`, `https`, `exec`. The scheme handler interprets paths under its prefix and implements the op surface. Consumption surface §scheme-surface; author contract: [plurnk-schemes](https://github.com/plurnk/plurnk-schemes). |
35
35
  | **mimetype** | A channel's content type. Drives the handler that produces the structural projections (`symbols`, `deepJson`, `deepXml`). Consumption surface §mimetype-surface; author contract: [plurnk-mimetypes](https://github.com/plurnk/plurnk-mimetypes). |
36
36
  | **provider** | An LLM transport. Implements `generate({messages, signal})` against a wire protocol. Consumption surface §provider; author contract: [plurnk-providers](https://github.com/plurnk/plurnk-providers). |
@@ -43,7 +43,7 @@ Independent axes on entries and channels. Confusion across them is a recurring s
43
43
  |---|---|---|
44
44
  | **status** | HTTP int | Outcome of an operation. Carried on `log_entries.status_rx`, returned from op handlers. Per the catalogue (§send-dispatch). |
45
45
  | **channel state** | `static \| active \| closed \| errored` | Streaming lifecycle of a channel's content. Metadata, not gating — engine renders content regardless of state. |
46
- | **entry state** | `proposed \| resolved \| cancelled` | Proposal lifecycle. `proposed` = pending client accept; `resolved` = side effect happened; `cancelled` = client rejected. Distinct from channel state. |
46
+ | **entry state** | `proposed \| resolved \| failed \| cancelled` | Proposal lifecycle (`log_entries.state`). `proposed` = pending client accept; `resolved` = accepted, side effect happened; `failed` = rejected (no effect); `cancelled` = the proposal was cancelled (loop abandoning). Distinct from channel state. |
47
47
  | **outcome** | `string \| null` | Short reason for `failed`/`cancelled` (`"permission:403"`, `"aborted"`, `"not_found"`). Opaque to most callers. |
48
48
 
49
49
  ### §authority-terms Writer / authority
@@ -64,8 +64,8 @@ Independent axes on entries and channels. Confusion across them is a recurring s
64
64
  | **sudden death** | The last `MAX_STRIKES` turns of a loop's `MAX_LOOP_TURNS` window emit soft 429 warnings so the model can wrap up cleanly. `soft=true`: no strike, no streak increment. |
65
65
  | **mode** | `"ask" \| "act"`. Per-loop. Ask = read-only (no side-effecting ops); act = full surface. |
66
66
  | **flag** | Per-loop boolean shaping the active toolset: `yolo` (auto-accept proposals), `noWeb`, `noInteraction`, `noProposals`. |
67
- | **proposal** | A deferred side-effecting action awaiting client accept/reject (full lifecycle §proposal). State machine: `proposed → resolved` or `proposed cancelled`. `yolo` short-circuits to immediate. |
68
- | **resolution** | Client's accept/reject of a proposal via `op.resolve` RPC. |
67
+ | **proposal** | A deferred side-effecting action awaiting client accept/reject (full lifecycle §proposal). State machine: `proposed → resolved` (accept), `→ failed` (reject), or `→ cancelled` (cancel). `yolo` short-circuits to immediate. |
68
+ | **resolution** | Client's accept / reject / cancel of a proposal via the `loop.resolve` RPC (§methods). |
69
69
 
70
70
  ### §packet-terms Packet terms
71
71
 
@@ -99,10 +99,10 @@ Dependency direction (from root to leaf):
99
99
  - **`plurnk-grammar`** — root. Owns the JSON-Schema contracts (Packet, TelemetryEvent, AST shapes), the ANTLR parser that turns model output into `PlurnkStatement[]`, and `PlurnkParseError` with its `toTelemetryEvent()` helper. Nothing in the ecosystem can speak the DSL without it; everything else pins it exactly.
100
100
  - **Framework siblings** consume grammar and define their own author-facing contracts:
101
101
  - `plurnk-providers` — Provider/Alias types, `parseAliasesFromEnv`, `resolveActiveAlias`, `Mock`, `ProviderUsage` (currency-aware, includes `reasoning`). Vendor-specific implementations are children: `plurnk-providers-openai`, `-google`, `-ollama`, `-openrouter`, `-cloudflare`, `-xai`.
102
- - `plurnk-mimetypes` — handler base classes, discovery, fitting algorithm, matcher dispatch. Handler children are per-mimetype: `plurnk-mimetypes-text-{python,typescript,markdown,html,csv,plain}`, `plurnk-mimetypes-application-{json,yaml,toml,pdf}`, …
102
+ - `plurnk-mimetypes` — handler base classes, discovery, the fitting algorithm, and the match primitives (`queryGlob`/`queryRegex`/`queryJsonpathObject`/`queryXpathString`) the service's matcher dispatches over (§matcher-dispatch). Handler children are per-mimetype: `plurnk-mimetypes-text-{python,typescript,markdown,html,csv,plain}`, `plurnk-mimetypes-application-{json,yaml,toml,pdf}`, …
103
103
  - `plurnk-schemes` — scheme-author types (`SchemeManifest`, `WriterTier`, `LoopFlags`), result-shape contracts (`EntryResult` / `ProposalResult` / `PassthroughResult`), slicing primitives, matcher helpers, `schemeError(...)` constructor. Future scheme children: `plurnk-schemes-http`, `plurnk-schemes-git`, …
104
104
  - `plurnk-execs` — `BaseExecutor`, `SubprocessExecutor`, runtime resolver, discovery. Children declare runtimes: `plurnk-execs-sh`, future `plurnk-execs-search`, `plurnk-execs-node`, …
105
- - **`plurnk-service`** (this repo) — consumes all of the above. Implements the engine, dispatches ops through scheme handlers, hosts the in-tree set of schemes (`plurnk`, `log`, `exec`, `known`, `unknown`, `skill`, `file`), discovers installed mimetype handlers + provider vendors + executor siblings at boot, hosts the daemon (`src/service.ts` over WebSocket + JSON-RPC), and projects packets to the wire per `Packet.json`. Most of the substantive runtime work lives here.
105
+ - **`plurnk-service`** (this repo) — consumes all of the above. Implements the engine, dispatches ops through scheme handlers, hosts the in-tree set of schemes (`plurnk`, `log`, `exec`, `known`, `unknown`, `skill`, `file`), discovers installed mimetype handlers + provider vendors + executor siblings at boot, hosts the daemon (`src/service.ts` over WebSocket + JSON-RPC), and projects packets to the wire (the packet shape is service-owned since grammar 0.67 deleted `Packet.json`, §packet). Most of the substantive runtime work lives here.
106
106
  - **`plurnk`** (client) — terminal UI consuming the daemon's RPC surface. Renders `telemetry/event` notifications, subscribes to log/stream/proposal events. No engine logic of its own.
107
107
 
108
108
  The grammar is the contract. The frameworks consume the contract and add author-facing surfaces. The service consumes the frameworks and runs the engine. The client consumes the service and renders to humans. Each tier is its own published package; each tier's evolution happens in its own repo.
@@ -138,11 +138,11 @@ Server posture: this package is the runtime. User-facing CLI lives in `plurnk` a
138
138
 
139
139
  **Wild west — no mutual exclusion.** Runs share the manifest without locks. Coordination is cooperative (tags + the shared workspace convention) and softly fenced (the §membership `read-only` overlay, a session policy, bounds every run's writable surface uniformly — §machine-processes); a conflict *surfaces* as a delta rather than being prevented. Inform, never override. {§actor-boundary-no-mutex}
140
140
 
141
- **Passive wake.** An idle run wakes on exactly two events, both *directed at the run*: a prompt injected into it — the **voice door** (a user/system `loop.inject`, and once `run://` lands a sibling's `SEND(run:///self)`) — or a **stream-status transition** on a subscription it opened (§channel-state). Everything ambient is a delta — a sibling's edit to a shared entry, an out-of-band disk change — and a delta **never** wakes; it queues and drains at the next turn one of those two events produces (§env-delta). {§actor-boundary-passive-wake}
141
+ **Passive wake.** An idle run wakes on exactly two events, both *directed at the run*: a prompt injected into it — the **voice door** (a user/system `loop.inject`, and once `run://` lands a sibling's `SEND(run://.)`) — or a **stream-status transition** on a subscription it opened (§channel-state). Everything ambient is a delta — a sibling's edit to a shared entry, an out-of-band disk change — and a delta **never** wakes; it queues and drains at the next turn one of those two events produces (§env-delta). {§actor-boundary-passive-wake}
142
142
 
143
143
  **Self-hosting — the runtime is an actor, not a back channel.** Runtime-initiated work (fs reconciliation §membership, git auto-add) is an **ephemeral `plurnk` run** firing ordinary ops, seen by other runs through the environment door like any actor's — not a privileged engine pathway. The engine keeps only the irreducible kernel runs stand on (spawn, dispatch, packet assembly, the budget rails §grinder, the fs-watch); everything expressible as ops on session entries is a run doing ops, through the same `op.*` surface (§methods) the service offers clients. Dogfooding is the architecture, not a test mode. {§actor-boundary-self-hosting}
144
144
 
145
- **Migration path.** Largely realized: `Engine.dispatch` is origin-agnostic; client ops run in a per-connection client loop (`_dispatchAsClient`); plurnk EDITs already carry `origin=plurnk`. The keystone is **built** — `dispatchAsPlurnk` spawns the session's reserved `plurnk` run and fires ops through dispatch, mirroring `_dispatchAsClient`. What remains is *repatriation* — the inline plurnk dispatches still bolted into the model's loop (the §env-delta materialization, the manifest build, git auto-add) move onto it.
145
+ **Migration path.** Largely realized: `Engine.dispatch` is origin-agnostic; client ops run in a per-connection client loop (`_dispatchAsClient`); plurnk EDITs already carry `origin=plurnk`. The keystone is **built** — `dispatchAsPlurnk` spawns the session's reserved `plurnk` run and fires ops through dispatch, mirroring `_dispatchAsClient`; its uses so far (operator docs below; the fs-divergence narration) land in the plurnk run's log. The line that remains is one of *kind*, not a list of pending dispatches: work **expressible as an op** belongs on the keystone; work that is **not** stays kernel. Disk→entry materialization is the latter *ingestion* is the inverse of an EDIT (which proposes egress to disk, §membership-edit-membership-gate), so it has no actor op and remains fs-watch kernel, paired with the plurnk run's filtered `source=file` narration (§env-delta) so a sibling pulls only true divergences, not every re-sync; the manifest build is likewise the per-turn derivation pump — packet-assembly kernel, not an entry-creating op. The one outstanding *expressible* piece is **git auto-add** a model-created file surfaced as a plurnk-run op — gated on the §membership repo-overlay still being built.
146
146
 
147
147
  **The keystone's first use: operator reference docs.** `PLURNK_MD_<ALIAS>=<path>` (§operator-config) materializes `<path>` as a `plurnk:///<ALIAS>.md` entry — a `dispatchAsPlurnk` EDIT in the plurnk run, **not** the model's — and the model's turn-0 foists a READ of it. The model reads the doc inline while the materializing EDIT stays out of its log: idiomatic context injection, an ordinary entry + READ rather than a bespoke packet section. The same `PLURNK_MD_*` convention cascades to clients. {§actor-boundary-doc-injection}
148
148
 
@@ -152,13 +152,13 @@ Server posture: this package is the runtime. User-facing CLI lives in `plurnk` a
152
152
 
153
153
  **Question.** §actor-boundary isolates runs and lets the runtime self-host, but it stands on an ownership model it never states: what does a *session* own versus a *run*; what is shared versus private; and what does a fork carry? Unstated, the downstream questions — which run `log.read` reads, what a fork copies, where a per-client window onto the workspace would live — grow subtle, then metastasize. Drawn once, they vanish.
154
154
 
155
- **Decision — the session is the world; a run is a log on it.** A **session** is the world: one shared filesystem — the `session`-scoped entries, surfaced as `plurnk:///manifest.json` (§packet) — under one membership overlay (§membership). Exactly one filesystem and one overlay per session; neither is per-run. A **run** is a process whose private memory is its **log** (§lifecycle-terms) — its loops, turns, and rows, each row carrying its own content, attribution (`origin`/`source`, §env-delta), and fold-state (`expanded`). A run owns **no membership**; even its visibility is not a possession but a bit on its own rows. It is a *history over the shared world, not a world*.
155
+ **Decision — the session is the world; a run is a log on it.** A **session** is the world: one shared filesystem — the `session`-scoped entries, surfaced as the per-scheme catalog (`FIND(scheme:///**)`, §packet) — under one membership overlay (§membership). Exactly one filesystem and one overlay per session; neither is per-run. A **run** is a process whose private memory is its **log** (§lifecycle-terms) — its loops, turns, and rows, each row carrying its own content, attribution (`origin`/`source`, §env-delta), and fold-state (`expanded`). A run owns **no membership**; even its visibility is not a possession but a bit on its own rows. It is a *history over the shared world, not a world*.
156
156
 
157
157
  **One filesystem.** The entries are the session's: `entries.session_id`, never a run. A write by any run is a write to the one filesystem every run reads; there is no per-run entry set. {§machine-processes-one-filesystem}
158
158
 
159
159
  **One overlay.** Membership — `git ls-files ∪ pick − hide` with `view` read-only (§membership) — is the session's: `session_constraints.session_id`, never a run. It is workspace *curation*, and the workspace *is* the session; two runs are two conversations about one curated workspace and see the same one. Divergent membership is a different session, never a per-run overlay. {§machine-processes-one-overlay}
160
160
 
161
- **A run is its log — and nothing beside.** The run-private state is the log and only the log. *What I am looking at* (OPEN/FOLD) is `log_entries.expanded`, a bit on the run's own rows, toggled by ordinary `log:///` ops — not a second store, and never membership (§open-fold). *What I last saw* needs no shadow either: a run learns its world moved through log entries (§env-delta) — a sibling's write broadcast into its log, an out-of-band disk change detected against the entry's own content and broadcast the same way — never through a per-run snapshot the run cannot see. The log is the whole of a run's memory. {§machine-processes-run-is-its-log}
161
+ **A run's memory of the world is its log — no shadow beside it.** A run's view of the shared world is the log and only the log — never a per-run snapshot. *What I am looking at* (OPEN/FOLD) is `log_entries.expanded`, a bit on the run's own rows, toggled by ordinary `log:///` ops — not a second store, and never membership (§open-fold). *What I last saw* needs no shadow either: a run learns its world moved through log entries (§env-delta) — a sibling's write broadcast into its log, an out-of-band disk change detected against the entry's own content and broadcast the same way — never through a per-run snapshot the run cannot see. (Its private **scratch** run-scope entries, §run-scheme — is the run's own evolving workspace, owned not shadowed: a store it writes and reads deliberately, not a hidden mirror of the shared world. The doctrine is *no shadow of the world*, not *no private state*.) {§machine-processes-run-is-its-log}
162
162
 
163
163
  **A run's log is private to packets, not to the session.** Isolation (§actor-boundary) governs what an *actor* sees — its own run, never a sibling's. It does not wall off the *wire*: any connection may read any run's log in its session by id — `log.read({ runId })`, ownership-verified, defaulting to the connection's own run. This is how a conversation client reads the **model** run, where the conversation lives: `loop.run` returns its `modelRunId`, and `session.runs` enumerates a session's runs for a connection that did not drive it live. The read is observation, never packet membership — no actor sees it. {§machine-processes-model-run-readable}
164
164
 
@@ -172,21 +172,23 @@ Server posture: this package is the runtime. User-facing CLI lives in `plurnk` a
172
172
 
173
173
  **Migration path.** Mostly stating what the schema already carries: `runs.parent_run_id` and the parentless `sessions` exist (§lifecycle-terms); `session_constraints` is session-level (§membership); §env-delta already makes a run's timeline self-contained, so a fork's log copy suffices. Additive: `run.fork` over the wire (the engine fork is built). Two repatriations: §actor-boundary's "read-only overlay scopes a run's writable surface" becomes a *session* policy bounding every run uniformly; and the §env-delta environment door has shed its per-run snapshot — a run's only memory is its log, so drift is pulled from the shared log (other actors' edits since the run's last turn) and the filesystem narrates its own through the `plurnk` run, both already log entries, never a per-run shadow.
174
174
 
175
- ### §run-scheme The run:// scheme — spawn, irc, fork, terminate, cap, collect
175
+ ### §run-scheme The run:// scheme — control (spawn, irc, fork, terminate, cap, collect) and run-scope scratch
176
176
 
177
- The run:// scheme makes §machine-processes addressable: a `run://` target is a sister run in the session — `run:///.` the current run, `run:///<name>` a session-scoped sibling (`runs.name`). Same-session only; a run never addresses another session's runs (§actor-boundary). Three ops, fire-and-forget — the child runs independently, lineage in `runs.parent_run_id`:
177
+ The run:// scheme makes §machine-processes addressable: a `run://` target is a sister run in the session — `run://.` the current run, `run://<name>` a session-scoped sibling (`runs.name`). Same-session only; a run never addresses another session's runs (§actor-boundary). The path discriminates the two faces: **path-absent is control** on the run-as-actor — the NAME is the authority (`run://<name>`, two slashes, no path) — while **path-present is run-scope storage**: `run://<owner>/<path>` (or `run:///<path>` for self) addresses the owner's private scratch (Scratch + Perspective, below). The control ops are three, fire-and-forget — the child runs independently, lineage in `runs.parent_run_id`:
178
178
 
179
- - **Spawn** — `EDIT(run:///<name>):prompt` creates a new sister (empty log) and starts it with `prompt` on its first loop; self cannot be spawned (400). {§run-scheme-spawn}
180
- - **irc** — `SEND(run:///<name>):msg` delivers `msg` to an existing sister, the **voice door** (§actor-boundary-two-doors): an active sister folds it into its next turn, an idle one wakes (§actor-boundary-passive-wake); a name with no run in the session is 404. {§run-scheme-irc}
181
- - **Fork** — `COPY(run:///<src>):prompt` branches `src` (self when `.`): the source's log is deep-copied into a new sister (§machine-processes-fork-copies-the-log), which continues with `prompt`; the world is shared, never copied (§machine-processes-fork-shares-the-world). {§run-scheme-fork}
179
+ - **Spawn** — `EDIT(run://<name>):prompt` creates a new sister (empty log) and starts it with `prompt` on its first loop; self cannot be spawned (400). {§run-scheme-spawn}
180
+ - **irc** — `SEND(run://<name>):msg` delivers `msg` to an existing sister, the **voice door** (§actor-boundary-two-doors): an active sister folds it into its next turn, an idle one wakes (§actor-boundary-passive-wake); a name with no run in the session is 404. {§run-scheme-irc}
181
+ - **Fork** — `COPY(run://<src>):prompt` branches `src` (self is `run://.`): the source's log is deep-copied into a new sister (§machine-processes-fork-copies-the-log), which continues with `prompt`; the world is shared, never copied (§machine-processes-fork-shares-the-world). A fork ALSO inherits the source's run-scope **scratch** — its private workspace deep-copied with the owner remapped (source → branch) — so the branch opens with the parent's notes and diverges on its own edits: *fork = everything-in-common-but-name*. {§run-scheme-fork} {§run-scheme-fork-scratch}
182
182
 
183
183
  All three ride one engine seam — the daemon's inject (active→fold, idle→enqueue+drain) — so the handler creates/branches/resolves the run and hands off; the daemon owns provider + system prompt. COPY's body here is the fork's seed prompt, not a destination path: the engine routes a run:// source away from the entry-copy path before parsing the body.
184
184
 
185
185
  Beyond the three creation ops:
186
186
 
187
- - **Terminate** — `KILL(run:///<name>)` aborts a run by address (self when `.`): its active loop closes 499 and its subscriptions tear down; a name with no run is 404. The override to the fire-and-forget default not a parent-power, whoever holds the address may end it; a run left alone simply ends at its own `SEND[200]`. {§run-scheme-terminate}
187
+ - **Scratch (storage)** — `run://<owner>/<path>` is run-scope entry storage (`scope='run'`, the owner is the run name folded into the path; `run:///` is self). A run EDITs only its own scratch a cross-run write is **403** ("read a sister's notes, never write them") but READs and FINDs any sister's by name (cross-run read is open; scratch is perspective-private, not ACL-private). {§run-scheme-scratch}
188
+ - **Perspective** — a run's own scratch is catalogued in **its** manifest alone — `Manifest(run) = session-scope ∪ this-run's-run-scope`, foisted as `FIND(run:///**)` at turn 0 — so a sibling reaches it only by explicit `FIND(run://<name>/**)` and never sees it in its own perspective; isolation is structural (`scope='run'` is excluded from every session query, the owner opted back in only on its own read paths). {§run-scheme-find-perspective}
189
+ - **Terminate** — `KILL(run://<name>)` aborts a run by address (self is `run://.`): its active loop closes 499 and its subscriptions tear down; a name with no run is 404. The override to the fire-and-forget default — not a parent-power, whoever holds the address may end it; a run left alone simply ends at its own `SEND[200]`. {§run-scheme-terminate}
188
190
  - **Cap** — `PLURNK_SESSION_RUNS_MAX_ACTIVE` ceilings the *concurrent* active runs per session (a run with a non-terminal loop); a spawn or fork past it fails hard (508 — no queue, no retry), irc exempt; `-1` disables it. The fork-bomb brake, sized for sessions that live for months. {§run-scheme-cap}
189
- - **Collect** — a run's loop reaching a terminal status surfaces to its sisters as an ambient FOLDED delta (§env-delta): a `SEND` from `run:///<name>` carrying the loop's deliverable — the `SEND[200]` body, or for an abandonment the reason. Every death-path is stamped uniformly, so no termination is silent; collection is the shared world moving, never a verb. {§run-scheme-collect}
191
+ - **Collect** — a run's loop reaching a terminal status surfaces to its sisters as an ambient FOLDED delta (§env-delta): a `SEND` from `run://<name>` carrying the loop's deliverable — the `SEND[200]` body, or for an abandonment the reason. Every death-path is stamped uniformly, so no termination is silent; collection is the shared world moving, never a verb. {§run-scheme-collect}
190
192
 
191
193
  ### §run-lifecycle Run lifecycle: the drain, the reap, the passive wake
192
194
 
@@ -197,6 +199,8 @@ A run is a **log plus a cancellation scope** — one `AbortController` per run,
197
199
  - **A stream's kill binds to the scope it captured at spawn.** A stream captures the run's cancellation scope as it registers and wires its kill to it, re-checking `aborted` AFTER wiring — no check-then-listen gap can drop an abort that lands mid-registration. Because the scope is replaced only once aborted, a captured-then-replaced scope is necessarily already aborted, so replacement never strands a live stream. {§run-lifecycle-exec-epoch-bound}
198
200
  - **A cancelled run is not resurrected by its own torn-down work.** A stream conclusion delivered to a cancelled, idle run starts no fresh drain: an aborted (499) conclusion is skipped, and a straggler that concluded cleanly surfaces its deliverable as an environment delta (§env-delta), never a revived loop. The cancel was deliberate; only an explicit `loop.run` resumes the run. {§run-lifecycle-no-resurrection}
199
201
  - **A stream conclusion always reaches its run.** When a backgrounded stream concludes, the daemon routes it through the same inject seam as any loop source (§actor-boundary-passive-wake): an active run folds the conclusion into its next turn; a run parked at a **slept loop** (`SEND[202]`) **awakens that loop in place** — the slept loop *is* the continuation, so there is no fresh loop and no summary-as-prompt fiction. The result is never lost: a parked loop sleeps rather than ending, and the stream's status-transition is the OPEN event (§actor-boundary-passive-wake) that wakes it; on resume it reads the concluded stream's own state, not a synthetic prompt. {§run-lifecycle-wake-liveness}
202
+ - **A child run concluding wakes a parent parked on it — the topology join.** `run://` spawn/fork records `parent_run_id` (§lifecycle-terms). When a run's drain exits having **concluded** — no parked `202` loop, no open stream — the daemon resumes its parent **in place** if the parent is parked (`#onDrainExit` → the shared `#wakeParkedRun`, the same 202→100 resume a stream conclusion uses). So a parent that spawns work and `SEND[202]`s is woken the moment its child finishes; on resume it reads the child's deliverable from the §run-scheme-collect delta in its own log — a control edge, **never an injected prompt**. The wake recurses upward via the parent's own drain-exit. A child still running — or itself parked at 202 — is not *concluded*, so it does not wake the parent (it's still a live thing the subtree holds). This is the structured-concurrency join: streams and child runs are the same kind of "live thing a run holds," driving premature-terminate (§send-premature-terminate), the wake edge, and the collect delta identically. {§run-lifecycle-child-wake}
203
+ - **A 202 whose subtree is idle emits `loop/quiesced` — a soft signal, not a terminal.** When a loop parks at `SEND[202]` and its subtree is idle (no open stream, no non-terminal child), the daemon broadcasts `loop/quiesced` (§notifications) — the client's honest *"nothing is running under this run right now."* The loop **stays at 202 and is reawakable**: a later `irc`/`loop.run` resumes it (and it re-quiesces, re-firing). It is **never a terminal code** — in a topology where any sibling can `irc` any run, true finality below the session doesn't exist, so quiescence (not finality) is the honest "done." A subtree with any live thing emits nothing — that thing's conclusion is the wake edge, not a quiesce. This is also the dead-park resolution: a wake-edge-less 202 no longer hangs silently; it announces idleness while staying resumable. {§run-lifecycle-quiesced}
200
204
  - **A loop is never stranded by a drain's exit.** A drain relinquishes its registry slot only after a lock-held re-claim confirms the queue is empty; a loop enqueued during that teardown is either re-claimed by the exiting drain or claimed by a fresh drain that a later inject starts. The relinquish and the start are serialized, so neither the lost-loop hang nor a transient double-drain can occur. {§run-lifecycle-no-lost-loop}
201
205
 
202
206
  ---
@@ -209,12 +213,14 @@ Author-facing contract: [plurnk-providers#1](https://github.com/plurnk/plurnk-pr
209
213
 
210
214
  Three entry points:
211
215
 
212
- - `provider.generate({messages, signal})` — once per turn; returns `{ assistant: { content, reasoning, usage, finishReason, model }, assistantRaw }`. **Engine parses `assistant.content`** into `PlurnkStatement[]` via `@plurnk/plurnk-grammar`. {§provider-surface-generate}
216
+ - `provider.generate({messages, signal})` — once per turn; returns `{ assistant: { content, reasoning, usage, finishReason, model }, assistantRaw, meta? }`. **Engine parses `assistant.content`** into `PlurnkStatement[]` via `@plurnk/plurnk-grammar`. {§provider-surface-generate}
213
217
  - `provider.countTokens(text)` — synchronous, called at write-time (§tokenomics) and render-time. Non-negative integer. {§provider-surface-counttokens}
214
218
  - `provider.costFor(usage)` — once per completed turn; pico-USD. Engine writes to `turns.usage_cost_pico`; triggers cascade to `runs.cost_pico` / `sessions.cost_pico`. {§provider-surface-costfor}
215
219
 
216
220
  Plus immutable identity: `provider.contextSize` (token total, or `null` → "no budget info"), read by the budget {§provider-surface-identity}; and `provider.model` — the instance identity the deferred model-switch recompute compares (§tokenomics), exposed but not yet consumed here.
217
221
 
222
+ **Metadata passthrough (provider → client).** `generate` may return an open `meta: Record<string, unknown>` bag — e.g. a hosted provider's running `balancePico`. The service stores it **unenforced** per turn (`turns.meta`, `json_valid` only — no schema) and forwards the latest turn's blob to the client on the loop usage payload (`loop.run` result / `loop/terminated`, §methods). **The service never reads a field within it.** The canonical-field contract — which fields exist and their shapes — is the *provider framework's* (it normalizes raw vendor data into the agreed set) and the *client's* (it renders that set); a provider and client can ship a feature with **zero service change** as long as the blob flows. Absent → `{}` (the client renders nothing; never fabricated). The mirror direction (client → provider, the self-identified `client` id) rides `generate({client})` (§attribution). {§meta-passthrough}
223
+
218
224
  ### §provider-guarantees Engine → provider guarantees
219
225
 
220
226
  - `messages` is a complete prompt (the section list, pre-assembled into the system + user messages). Provider does not reorder.
@@ -267,7 +273,7 @@ Every op targets a URI; the entry key is `(scope, scheme, pathname)`. The URI pa
267
273
 
268
274
  - A **registered** scheme is a plurnk namespace: its authority is a leading path segment, folded into the pathname (`Engine.#extractTarget` → `foldAuthorityIntoPath`). So `known://x`, `known:///x`, and pathname `/x` are the same entry — the authority is never a host, and the two-slash and three-slash forms are not distinct resources. {§scheme-address-namespace-fold}
269
275
  - A **foreign** scheme (unregistered — `http`/`https`) is a real web host: its authority stays in `hostname`, never folded.
270
- - `file` persists `scheme = NULL`; a relative path resolves against the workspace root to the namespace-absolute `/rel` key (RFC §5 reference resolution), and a path escaping the root is 403 (§membership).
276
+ - `file` persists `scheme = NULL`; a relative path resolves against the workspace root to the namespace-absolute `/rel` key (RFC 3986 §5 reference resolution), and a path escaping the root is 403 (§membership).
271
277
 
272
278
  Storage keys on the resolved `(scheme, pathname)` **verbatim** — the leading slash is the namespace origin, not a filesystem absolute, and is never re-normalized at the storage boundary.
273
279
 
@@ -327,15 +333,20 @@ interface PlurnkSchemeContext {
327
333
  readonly runId: number;
328
334
  readonly loopId: number;
329
335
  readonly turnId: number;
330
- readonly writer: "model" | "client" | "plurnk" | "plugin";
336
+ readonly writer: "model" | "client" | "plurnk" | "plugin"; // WriterTier
331
337
  readonly signal: AbortSignal | undefined;
332
338
  readonly streamEventNotify?: StreamEventNotify;
333
339
  readonly wakeRunNotify?: WakeRunNotify;
340
+ readonly injectRun?: InjectRunNotify; // run:// spawn/fork/irc loop-start (§run-scheme)
334
341
  readonly mimetypes?: Mimetypes;
342
+ readonly executors?: ExecutorRegistry; // boot-discovered EXEC runtimes (§exec)
343
+ readonly tokenize?: (text: string) => number; // write-time tokenizer (§tokenomics)
344
+ readonly defaultChannelFor?: (scheme: string | null) => string;
345
+ readonly pushTelemetry?: (event: TelemetryEvent) => void; // → next packet errors[] + telemetry/event (§telemetry)
335
346
  }
336
347
  ```
337
348
 
338
- Notifier fields populated by the Daemon; absent in test fixtures.
349
+ The optional engine-/daemon-populated capabilities (the notifiers, `injectRun`, `executors`, `tokenize`, `defaultChannelFor`, `pushTelemetry`) are absent in bare test fixtures; a handler that needs one **fail-hards** rather than silently degrading (no default runtime, no silent zero-token write).
339
350
 
340
351
  Engine → scheme guarantees:
341
352
 
@@ -345,7 +356,7 @@ Engine → scheme guarantees:
345
356
  - `ctx.signal` is wired to the run's AbortController (§provider-guarantees-signal-wired).
346
357
  - Scheme exceptions become the action-entry's outcome (status 500); summary surfaces in next turn's `errors` section (§telemetry). {§scheme-surface-exception-500}
347
358
 
348
- **Tokenization participation.** Schemes route writes through the shared `_entry-crud.ts` write helper (in plurnk-service today; migrates to plurnk-schemes). Helper populates `entry_channels.tokens` at write time via `ctx.provider.countTokens` (§tokenomics-tokens-stored-at-write). Raw DB writes bypass tokenization — out of API scope.
359
+ **Tokenization participation.** Schemes route writes through the shared `_entry-crud.ts` write helper (in plurnk-service today; migrates to plurnk-schemes). Helper populates `entry_channels.tokens` at write time via `ctx.tokenize` (§tokenomics-tokens-stored-at-write). Raw DB writes bypass tokenization — out of API scope.
349
360
 
350
361
  ---
351
362
 
@@ -409,7 +420,7 @@ new Mimetypes({
409
420
 
410
421
  Fallback heuristic is a boot-before-provider-resolved tripwire.
411
422
 
412
- **Manifest build.** `EntryManifest.buildManifestBody` is the engine-side packet-assembly pass (the §mimetype firing point) that walks **every** entry. It calls `process({ content, hint })` per channel for the catalog's `lines` (`totalLines`) and, for the body channel, pulls `symbols`+`references` from the *same* call to (re)build the `@graph` symbol index (`symbol_defs`/`symbol_refs`) via `EntryGraph.populateFrom` — one parse, two projections:
423
+ **Derivation pump.** `EntryManifest.maintainDerivations` is the per-turn engine-side pass (the §mimetype firing point) that walks **every** entry. It calls `process({ content, hint })` per channel for the catalog's `lines` (`totalLines`) and, for the body channel, pulls `symbols`+`references` from the *same* call to (re)build the `@graph` symbol index (`symbol_defs`/`symbol_refs`) via `EntryGraph.populateFrom` — one parse, two projections:
413
424
 
414
425
  ```ts
415
426
  const result = await mimetypes.process({ content: r.content, hint: r.mimetype }, { channels: ["symbols", "references"] });
@@ -437,7 +448,7 @@ Schemes MAY declare multiple channels (`exec`: stdout/stderr/stdin; `http`: body
437
448
 
438
449
  ### §no-visibility Entries carry no visibility
439
450
 
440
- Every entry is uniformly listed in `plurnk:///manifest.json` (§packet) and READable — entries have no per-run open/folded state. Context curation is the model's, on the **log** (via OPEN/FOLD, §open-fold), never on entries.
451
+ Every entry is uniformly listed in the catalog (`FIND(scheme:///**)`, §packet) and READable — entries have no per-run open/folded state. Context curation is the model's, on the **log** (via OPEN/FOLD, §open-fold), never on entries.
441
452
 
442
453
  ### §channel-mimetype Mimetype is a (scheme, channel) property — never a default
443
454
 
@@ -460,8 +471,8 @@ Rules:
460
471
  |---|---|
461
472
  | `known:///france/capital` | body (default) |
462
473
  | `known:///france/capital#symbols` | symbols |
463
- | `exec:///sh/1/1/2#stdout` | stdout |
464
- | `exec:///sh/1/1/2#stderr` | stderr |
474
+ | `sh:///1/1/2#stdout` | stdout |
475
+ | `sh:///1/1/2#stderr` | stderr |
465
476
  | `sse://feed/y#data` | data |
466
477
  | `log:///N/T/A` | (no channel concept; atomic log row) |
467
478
 
@@ -476,8 +487,8 @@ RPC params carry fragments inline via the `target` string (`{ target: "known:///
476
487
 
477
488
  ```
478
489
  <<notes.md:...:notes.md — file scheme (bare)
479
- <<exec:///sh/1/1/2:...:exec:///sh/1/1/2 — exec default (stdout)
480
- <<exec:///sh/1/1/2#stderr:...:exec:///sh/1/1/2#stderr — non-default
490
+ <<sh:///1/1/2:...:sh:///1/1/2 — exec output default (stdout)
491
+ <<sh:///1/1/2#stderr:...:sh:///1/1/2#stderr — non-default
481
492
  <<log:///1/1/0:...:log:///1/1/0 — atomic log row
482
493
  ```
483
494
 
@@ -517,7 +528,7 @@ AST: `{ op: "READ", target, body: MatcherBody | null, signal: tags | null, lineM
517
528
 
518
529
  - Returns channel content + mimetype {§read-read-content}, or 404 {§read-read-404}.
519
530
  - `lineMarker` slices per §slice-semantics.
520
- - `body` matcher dispatches through `Mimetypes.query` per §matcher-dispatch (all four dialects wired).
531
+ - `body` matcher dispatches through the in-tree `Matcher` per §matcher-dispatch (all four content dialects wired).
521
532
 
522
533
  ### §open-fold OPEN / FOLD
523
534
 
@@ -579,23 +590,25 @@ The engine's failure terminals — **500** (strike threshold) and **508** (cycle
579
590
  **Two engine error states verify the claim.** Neither is a status code the model learns; both are engine machinery (§engine-rails), pushed to the model as a steering hint on the next packet and **never** as the strike itself (the model sees errors that happened, never the engine's accounting — the gamification policy, §engine-rails). Each strikes (`turnErrors`) and lets the loop continue so the model can correct; a model that ignores the hint and keeps offending spins out to the engine's 500, seeing only the repeated hint, never the count. (Both live at `Engine.runLoop`'s turn close.)
580
591
 
581
592
  - **Idle turn** {§send-idle-turn} — a continuing turn (102) whose ops are only PLAN/SEND — no work op. The model continued with nothing to do. The steer, verbatim: *"If the turn's work is complete, terminate with 200. If awaiting a stream or run trigger, terminate with 202 to hibernate."*
582
- - **Premature terminate** {§send-premature-terminate} — a `SEND[200]` while the run holds a live stream or spawn (§subscriptions, §run-lifecycle-total-reap). The model declared done with work still running. The engine **downgrades the 200 to 102** — the turn continues *and the SEND's body is preserved* (it dispatches as a continue, never discarded) — and steers, verbatim: *"Attempted termination with active streams. Terminate with 202 to hibernate until stream completion, KILL(path) with 200 again to clean up, or 499 to fail."*
593
+ - **Premature terminate** {§send-premature-terminate} — a `SEND[200]` while the run holds a **live thing**: an open stream/spawn (§subscriptions, §run-lifecycle-total-reap) **or a non-terminal child run** (§run-lifecycle — children and streams are the same kind of live thing a run holds). The model declared done with work still running. The engine **downgrades the 200 to 102** — the turn continues *and the SEND's body is preserved* (it dispatches as a continue, never discarded) — and steers, verbatim: *"Attempted termination with active streams or child runs. Terminate with 202 to hibernate until they complete, KILL(path) with 200 again to clean up, or 499 to fail."*
583
594
 
584
- **Dead-park caveat — read before changing 202.** A 202 is a real hibernation only when a wake edge exists; a 202 with *no* wake edge sleeps forever. Today's guard is **preventive** un-advertising 202 plus the two steering states keep the model from declaring a spurious 202 **not** a daemon-side wake-edge check that terminates a wake-edge-less 202. That check is the known backstop, **deferred**. Until it lands, a model that emits 202 with nothing to wake it *can* hang; do not assume a wake-edge guarantee exists.
595
+ **Dead-park caveat — read before changing 202.** A 202 is a real hibernation only when a **wake edge** exists. Two edges exist: a **stream conclusion** (§run-lifecycle-wake-liveness) and an EXEC **poll cadence** `<T,P>` (§exec-poll — a per-run timer resumes the slept loop every P). grammar 0.74.20 now **advertises** `SEND[202]` on the hot path, so the old "preventive" guard (un-advertising it) no longer holds. The remaining gap is the **wake-edge-*less* 202** a model that parks with no open stream and no poll: it has nothing to resume it and *can* hang. The daemon-side check that detects and resolves a wake-edge-less 202 is the known backstop, still **deferred**; until it lands, do not assume a wake-edge guarantee for a 202 emitted with nothing running.
585
596
 
586
597
  ### §exec EXEC
587
598
 
588
- AST: `{ op: "EXEC", target (cwd), body: string | null (command), signal: string | null (runtime tag) }`.
599
+ AST: `{ op: "EXEC", target (cwd), body: string | null (command), signal: string | null (runtime tag), lineMarker (timeout/poll) }`.
589
600
 
590
601
  Engine routes unconditionally to `exec` scheme (path slot is `cwd`, not a URI). The runtime slot (`signal`) selects an executor, resolved against the boot-time `ExecutorRegistry` — siblings discovered and probed at startup, availability cached, default `sh`. Unknown or unavailable runtime → 501 carrying the probe `detail`. {§exec-registry-resolves}
591
602
 
592
- **Effect-gating.** Each executor declares an `effect` (`pure` | `read` | `host`); the service maps it to policy (`EffectPolicy`). A `host` runtime (subprocess; file-backed sqlite) mutates the host**propose** (lifecycle §proposal): the run waits for a human gate, then spawns and writes stdout/stderr to channels of an `exec:///<runtime>/<loop>/<turn>/<seq>` entry (the executor is the URI authority; the coordinate that follows matches the op's log-row coordinate, e.g. `exec:///sh/1/1/2`), returning `102 Processing` immediately. Channel state transitions (`active` `closed`/`errored`) drive what the model sees at subsequent turn boundaries (§channel-state). {§exec-host-proposes}
603
+ **Timeout and poll — `<T,P>` on the `<L>` slot (grammar 0.74.20).** EXEC repurposes the line-marker slot as `<timeout, poll>` in **seconds** (consistent with the `seconds=` stream-age render). `T` (mark[0]) caps the spawn's lifetime: at T the service aborts it (a bounded reap — polite signal then SIGKILL after `PLURNK_EXEC_KILL_GRACE_MS`) and stamps the stream **504**, distinct from a deliberate kill (499) or a clean exit (200). Absent / ≤0unbounded, the prior background-stream behavior. {§exec-timeout} `P` (mark[1]) is the hibernation **poll cadence**, stored on the subscription: while the loop is *parked* at `SEND[202]`, the daemon arms a per-run timer for the tightest open poll cadence and resumes the slept loop every P seconds to inspect progress (the same 202→100 resume a stream conclusion uses, §run-lifecycle). It does **nothing while the loop is active** an active loop already gets the ambient folded stream deltas exec-stream), so the poll-wake matters only across hibernation. A 202 with no polled stream gets no timer (it sleeps until a conclusion wakes it). {§exec-poll}
604
+
605
+ **Effect-gating.** Each executor declares an `effect` (`pure` | `read` | `host`); the service maps it to policy (`EffectPolicy`). A `host` runtime (subprocess; file-backed sqlite) mutates the host → **propose** (lifecycle §proposal): the run waits for a human gate, then spawns and writes stdout/stderr to channels of a `<runtime>:///<loop>/<turn>/<seq>` entry (the runtime tag is the URI scheme, §exec/#240; the coordinate matches the op's log-row coordinate, e.g. `sh:///1/1/2`), returning `102 Processing` immediately. Channel state transitions (`active` → `closed`/`errored`) drive what the model sees at subsequent turn boundaries (§channel-state). {§exec-host-proposes}
593
606
 
594
607
  A `read` runtime (observes external state, e.g. search) or `pure` runtime (no observable effect, e.g. `:memory:` sqlite) is side-effect-free → **auto-run**: no proposal, no human gate, no notification. It skips the gate a host command faces, but it does NOT resolve in-band — like every exec it backgrounds and streams, its output reaching the model through the environment-observation injector (a foisted READ of the stream's new bytes each turn, §exec-stream), never a same-turn receipt. {§exec-readpure-ungated}
595
608
 
596
609
  **Stream surfacing.** An exec's output is *observed, not fetched*. Each turn the environment-observation injector — the same machine §env-delta rides — reads each of the run's open channels from a per-channel byte cursor and foists the new bytes as an `origin=plurnk` READ at `<runtime>:///<coord>#<channel>`, then advances the cursor — each delta carries the `startLine` that cursor implies, so a stream spanning turns numbers into one continuous sequence (lines 1–k, then k+1–m), not a fresh `1:` each turn. The delta is **folded** while the channel streams and auto-**opened** on the terminal one (the channel closed): a model ignores a chatty background run but always SEES a finished one. It never types these READs — it consumes them. The EXEC row itself renders the *command* it ran, `:::`-fenced and line-numbered per §render-rule so the model can line-reference its own code — the input, distinct from the stream above (the output). This is exec as an instance of one ambient machine, env-delta as another (sibling edits, timestamp cursor, always folded). {§exec-stream}
597
610
 
598
- `SEND[499](exec:///<runtime>/<loop>/<turn>/<seq>)` cancels in-flight subprocess via subscription registry's stored AbortController (§stream-control).
611
+ `SEND[499](exec:///<loop>/<turn>/<seq>)` cancels the in-flight subprocess via the subscription registry's stored `AbortController` — the coordinate addresses the spawn (`exec://` is the process-control face); the `<runtime>://` output entry delegates the same KILL to the one `Exec` handler that owns the abort state (§stream-control).
599
612
 
600
613
  **Scoped environment.** An EXEC subprocess inherits the *project's* environment — its `.env`, the standard shell vars — so the model's commands run as the project expects; but never plurnk's own secrets: the provider API keys and `PLURNK_*` config are stripped before the spawn, so a model-run command can't `printenv` the engine's keys. The service owns the scoping policy (the denylist); the executor spawns with the env it is handed. {§exec-env-scoped}
601
614
 
@@ -656,7 +669,7 @@ Model sees lifecycle events in the `log` section per turn.
656
669
 
657
670
  ### §stream-constraints Engine constraints
658
671
 
659
- ONE engine-level constraint: **100 MiB char-length cap per channel body**. `CHECK (length(content) <= 104857600)` on `entry_channels.content` (migrations/001_schema.sql). Violations → SQLITE_CONSTRAINT; action-entry captures rejection at status 500.
672
+ ONE engine-level constraint: **100 MiB char-length cap per channel body**. `CHECK (length(content) <= 104857600)` on `entry_channels.content` (the genesis schema migration, `migrations/0000-00-00.01_schema.sql`). Violations → SQLITE_CONSTRAINT; action-entry captures rejection at status 500.
660
673
 
661
674
  All other limits are extrinsic — providers (request size, model context, fetch timeouts), schemes (per-call validation), mimetypes (render budgets). Engine does not throttle, batch, rate-limit, or cap anything else. {§stream-constraints-engine-one-cap}
662
675
 
@@ -676,8 +689,9 @@ SQLite (`node:sqlite`) with WAL mode and STRICT tables. Hand-written DDL; CI-ali
676
689
 
677
690
  No generator. SQLite-optimal: STRICT (3.37+), `INTEGER PRIMARY KEY` aliasing, explicit `NOT NULL`, indexed query paths, deliberate FK `ON DELETE`/`ON UPDATE`, `WITHOUT ROWID` where access pattern warrants, generated columns, FTS5.
678
691
 
679
- - One migration file per cohesive concern. Numbered, deterministic apply order.
680
- - `migrate` CLI: idempotent; skips applied markers in `applied_migrations`.
692
+ - One `.sql` file per cohesive concern under `migrations/`, **date-prefixed and basename-sorted** for deterministic apply order (`0000-00-00.01_schema.sql` is the genesis).
693
+ - DDL lives in `-- INIT: <name>` blocks (`CREATE TABLE IF NOT EXISTS`) that `@possumtech/sqlrite` runs idempotently at DB open — re-running is a no-op by construction, so today there is no separate `migrate` CLI or applied-marker table. A fresh DB is the recovery story while the project is greenfield-solo.
694
+ - **Forward — apply-once evolution.** The idempotent-`INIT` model creates a fresh schema but does not *evolve* a populated one. When persisted state outlives a "just nuke it" reset, the date-prefixed ordering is the foundation a real migration policy layers onto: ordered `ALTER` files applied once and recorded in a marker table, not recreated idempotently. The current scheme is chosen so that transition is **additive** — a marker table plus apply-once dispatch — never a restructuring of how DDL is authored or ordered.
681
695
  - **Schema-alignment test**: loads `@plurnk/plurnk-grammar/schema/*.json`, parses DDL via `node:sqlite` introspection, asserts every required schema field has a corresponding `NOT NULL` column. Grammar drift fails CI.
682
696
  - DDL = storage truth; JSON Schemas = wire truth. Tested-aligned, allowed to differ where ergonomics demand.
683
697
 
@@ -727,7 +741,7 @@ Env vars configure installed plugins; never declare existence. Filesystem is the
727
741
 
728
742
  Plugin discovery (§plugin-discovery) registers whatever's in `node_modules/@plurnk/*`.
729
743
 
730
- **Providers in-tree (`src/providers/`):** `Mock` (intg-only test fixture + worked example).
744
+ **Providers in-tree:** none. `Mock` (the intg-only test fixture + worked example) is a sibling, `@plurnk/plurnk-providers` (§mock-provider); `ProviderInstantiate.ts` dynamically imports the selected provider package.
731
745
 
732
746
  **Mimetypes in-tree:** none. Framework + handlers are all siblings.
733
747
 
@@ -763,7 +777,7 @@ Plugin discovery (§plugin-discovery) registers whatever's in `node_modules/@plu
763
777
  - Channel state (`active`/`closed`/`errored`) — subscription registry, not on `ChannelContent`.
764
778
  - Backpressure caps — none (§stream-constraints).
765
779
  - Stream cancel — `SEND[499]` (§stream-control).
766
- - Delete — MOVE to `/dev/null` (§move); `SEND[410]` also deletes as a side-effect (§send-dispatch).
780
+ - Delete — `KILL` (entry-KILL, the canonical delete; the MOVE→`/dev/null` idiom is retired, §move); `SEND[410]` also deletes as a side-effect (§send-dispatch).
767
781
  - Per-loop flags — `loops.flags` JSON column; `yolo` end-to-end today, others scheduled.
768
782
  - Default-channel wire rendering — §channel-selection.
769
783
 
@@ -788,9 +802,9 @@ Model selection: separate alias cascade in `ProviderRegistry` (§provider-instan
788
802
  | `PLURNK_MIN_CYCLES` | `3` | enforced | Min repetitions before cycle detection fires (§engine-rails). |
789
803
  | `PLURNK_MAX_CYCLE_PERIOD` | `4` | enforced | Max period length cycle detection examines (§engine-rails). |
790
804
  | `PLURNK_MD_<ALIAS>` | (unset) | enforced | Operator reference doc: materializes `<path>` as `plurnk:///<ALIAS>.md`, auto-READ into every model run's turn 0 (§actor-boundary). `~` expands to home. |
791
- | `PLURNK_MANIFEST_ITEMS` | `0` | enforced | Turn-0 manifest preview foisted into the model's first turn. `-1` = full `plurnk:///manifest.json`; positive `N` = the first N items (jsonpath slice); `0` / unset = off (§actor-boundary-manifest-preview). |
805
+ | `PLURNK_MANIFEST_ITEMS` | `0` | enforced | Turn-0 catalog preview foisted into the model's first turn, one `FIND(scheme:///**)` per active scheme. `-1` = each scheme's full catalog; positive `N` = its first N rows (`<L>` cap); `0` / unset = off (§actor-boundary-manifest-preview). |
792
806
  | `PLURNK_PROPOSAL_TIMEOUT_MS` | `300000` | enforced | ms wait for a proposed entry (status=202) to be resolved before timing out. |
793
- | `PLURNK_PROVIDERS_REASON_LEVEL` | `0` | enforced | Reasoning **magnitude** sent to the providers: `0` = none, positive = effort/budget the provider module translates to wire format (o-series tiers, Anthropic `budget_tokens`). The on/off is the `PLURNK_PROVIDERS_REASONING` gate. |
807
+ | `PLURNK_PROVIDERS_REASONING_BUDGET` | `-1` | enforced | The single provider reasoning gate (REQUIRED — fail-hard if unset). `0` = native reasoning off (in-DSL PLAN does it), `-1` = adaptive / no cap, `N` = capped at N tokens; provider modules translate per model family (reasoning_effort tiers, Anthropic `budget_tokens`). Floor (gemma): `0`. |
794
808
  | `PLURNK_FETCH_TIMEOUT` | `600000` | enforced | Service-wide ms ceiling on any outbound request (providers, future http schemes). Module-specific overrides are allowed below the ceiling. |
795
809
  | `PLURNK_DEBUG` | `0` | reserved | Schema-validation toggle. Not yet enforced. |
796
810
  | `PLURNK_LOG_LEVEL` | `info` | reserved | Stdout banner verbosity. Not yet enforced. |
@@ -798,7 +812,7 @@ Model selection: separate alias cascade in `ProviderRegistry` (§provider-instan
798
812
  **enforced** = engine reads and acts on the value. **reserved** = shipped in `.env.example` (forward-spec) but no-op until wired.
799
813
 
800
814
  **Two override semantics — ceiling vs default.** Which kind a var is determines what "override" means across the cascade:
801
- - **Ceiling** (most-restrictive-wins) — an operator-set hard bound nothing downstream may exceed: not a lower-precedence file, not a per-session constraint, not a per-call RPC arg. `PLURNK_GIT_ENABLED` (`=0` flatly denies git service-wide, §membership), `PLURNK_BUDGET_CEILING` (§tokenomics), `PLURNK_MAX_COMMANDS`, `PLURNK_MAX_STRIKES`, `PLURNK_FETCH_TIMEOUT` (module overrides allowed only *below* it), and `PLURNK_MAX_TURNS` (`-1` ships it off; a positive value caps the per-call request). The sandbox/cost guarantee: the operator caps it; no client widens it.
815
+ - **Ceiling** (most-restrictive-wins) — an operator-set hard bound nothing downstream may exceed: not a lower-precedence file, not a per-session constraint, not a per-call RPC arg. `PLURNK_GIT_ALLOWED` (`=0` flatly denies git service-wide, §membership), `PLURNK_BUDGET_CEILING` (§tokenomics), `PLURNK_MAX_COMMANDS`, `PLURNK_MAX_STRIKES`, `PLURNK_FETCH_TIMEOUT` (module overrides allowed only *below* it), and `PLURNK_MAX_TURNS` (`-1` ships it off; a positive value caps the per-call request). The sandbox/cost guarantee: the operator caps it; no client widens it.
802
816
  - **Default** (explicit-wins) — a fallback the most-specific setter replaces freely: `PLURNK_MODEL` (a `loop.run({alias})` overrides it), `PLURNK_REQUIREMENTS` (the per-call requirements default), and the config-time vars (`HOST` / `PORT` / `DB_PATH`).
803
817
 
804
818
  Enforcement is per-use-site — no central most-restrictive pass; each ceiling is checked where it bites. `PLURNK_MAX_TURNS` ships **off** (`-1` = no cap; the loop ends via SEND, budget, strikes, or cycle detection) and, when an operator sets a positive value, the per-call request is `min()`-capped against it. {§operator-config-max-turns-ceiling}
@@ -843,16 +857,16 @@ Success response: `{ "jsonrpc": "2.0", "id": …, "result": … }`. Failure: `{
843
857
  ### §method-registration Method registration
844
858
 
845
859
  ```ts
846
- registry.register("loop.run", {
860
+ registry.registerMethod("loop.run", {
847
861
  handler: async (params, ctx) => { /* ... */ },
848
862
  description: "Run a model-driven loop with a prompt.",
849
863
  params: {
850
864
  prompt: "string — the user prompt for the loop",
851
- sessionId: "number? — defaults to current attached session",
852
865
  maxTurns: "number? — defaults to PLURNK_MAX_TURNS",
866
+ alias: "string? — overrides the boot-time PLURNK_MODEL",
853
867
  },
854
868
  requiresInit: true,
855
- longRunning: true,
869
+ longRunning: false, // returns immediately (finalStatus:100); the loop runs async, §methods
856
870
  });
857
871
  ```
858
872
 
@@ -938,7 +952,7 @@ registry.register("loop.run", {
938
952
  | Method | Params | Result | Notes |
939
953
  |---------------|-------------------------------------|------------------------|-------|
940
954
  | `entry.read` | `target: string` | `{ status, entry }` | Read the full entry shape (channels + tags + metadata) at the given URI. {§methods-entry-read} |
941
- | `log.read` | `loopId?: number`, | `{ entries: LogEntry[] }` | Read recent log entries from the attached session, optionally filtered by loop. {§methods-log-read} |
955
+ | `log.read` | `loopId?`, `turnId?`, `loopSeq?`, `turnSeq?`, `sequence?`, `sinceId?`, `limit?` | `{ entries: LogEntry[] }` | Read recent log entries from the attached session. A full display coordinate (`loopSeq`+`turnSeq`+`sequence`) resolves the single entry behind an `L/T/S` waterfall line — full shape (tx + rx), server-side, no client fetch-all+match (#271). {§methods-log-read} |
942
956
 
943
957
  **Log coordinate.** Every `LogEntry` — from `log.read` and the `log/entry` notification alike — carries `loop_seq`/`turn_seq`, the loop+turn ordinals, beside the `loop_id`/`turn_id` DB keys, so a client renders the logical coordinate (e.g. `01/02/03`) without resolving ids. {§methods-log-coordinate}
944
958
 
@@ -976,6 +990,7 @@ Server-initiated events on the same WebSocket.
976
990
  |--------------------|-------------------------------------|------------|
977
991
  | `log/entry` | `{ entry: LogEntry }` | Every `log_entries` write. {§notifications-log-entry-notify} |
978
992
  | `loop/terminated` | `{ loopId, finalStatus, hitMaxTurns }` | Loop reaches terminal status. |
993
+ | `loop/quiesced` | `{ loopId, runId, status: 202 }` | A loop parked at `SEND[202]` reached subtree-quiescence (no open stream, no non-terminal child) — idle/complete-for-now but **reawakable**, distinct from `loop/terminated` (§run-lifecycle-quiesced). |
979
994
  | `loop/proposal` | `{ logEntryId, sessionId, runId, loopId, turnId, op, target, body, attrs, flags }` | Dispatch pauses on status=202. Carries `flags` so server-YOLO clients can suppress review UI. Client responds with `loop.resolve` (or `PLURNK_PROPOSAL_TIMEOUT_MS` fires). |
980
995
  | `session/created` | `{ id, name, projectRoot }` | Any client creates a session. |
981
996
  | `stream/event` | `{ entryId, channel, state, contentLength }` | Channel content grows or state transitions. {§notifications-stream-event-on-channel-change} |
@@ -1015,7 +1030,7 @@ Server-initiated events on the same WebSocket.
1015
1030
 
1016
1031
  **The client's run.** A client connection is an actor (§machine-processes); its `op.*` write to its **own run** — `origin = "client"`, one loop per connection — and `log.read` reads that run. Disconnect closes the loop's status; rows persist. Multiple connections each get their own client run.
1017
1032
 
1018
- `loop.run` and `inject` target the **model's run** — a separate run holding the conversation, `origin = "model"`. Both runs share the session's one filesystem (§machine-processes); the packet renders only the model's run, so the client's ops are structurally absent from it — no origin filter (§actor-boundary-isolation). *Migration:* the daemon today opens both loops in the connection's one run (the conflation §machine-processes corrects); the build gives the client and the model their separate runs.
1033
+ `loop.run` and `inject` target the **model's run** — a separate run holding the conversation, `origin = "model"`. Both runs share the session's one filesystem (§machine-processes); the packet renders only the model's run, so the client's ops are structurally absent from it — no origin filter (§actor-boundary-isolation). The model run (`Envelope.ensureModelRun`) and the connection's client run are distinct, each lazily allocated on first use the §machine-processes conflation is corrected.
1019
1034
 
1020
1035
  ### §errors Errors
1021
1036
 
@@ -1058,7 +1073,7 @@ Each entry: question, answer, rationale, migration path.
1058
1073
 
1059
1074
  **Question.** Rummy uses priority-ordered filter chains for packet assembly. Plurnk builds a default ordered section list directly in `Engine.#buildRequestPacket`, then lets trusted plugins rewrite it.
1060
1075
 
1061
- **Decision.** Two stages. (1) The engine builds the default section list — the kernel sections `definition`, `tools`, `schemes`, `log` (system slot), then `prompt`, `budget`, `errors`, `git`, `requirements` (user slot). (2) `SchemeRegistry.transformSections` pipes that list through every registered scheme that implements `transformSections(sections) → sections`, in registration order, before the engine measures. A plugin returns whatever list it wants — add, remove, reorder. {§packet-plugin-transform}
1076
+ **Decision.** Two stages. (1) The engine builds the default section list — the system-slot sections `definition`, `tools`, `schemes`, the policy sections `system-policy`/`project-policy`, and `budget` (budget is law — a privileged constraint kept in the lean system zone), then the user-slot sections `prompt`, `errors`, `log`, `git`, `requirements` (the log is short-term memory — data, rendered at the action point, not a privileged rule). (2) `SchemeRegistry.transformSections` pipes that list through every registered scheme that implements `transformSections(sections) → sections`, in registration order, before the engine measures. A plugin returns whatever list it wants — add, remove, reorder. {§packet-plugin-transform}
1062
1077
 
1063
1078
  **Why a whole-list transform, not a per-section hook.** It is the legible, fork-avoiding seam: a plugin that can reshape the packet to its needs never has a reason to fork the engine (§ecosystem). And it is **strictly in-process and trusted** (behind `PLURNK_PLUGINS_TRUSTED_ONLY`) — the client/RPC wire never reaches the packet, because handing an untrusted connection the model's entire context is exactly the actor-boundary violation the engine exists to prevent. Pure list-in/list-out; no context is handed to plugins.
1064
1079
 
@@ -1081,7 +1096,7 @@ Each entry: question, answer, rationale, migration path.
1081
1096
  - **Heaviest entries.** A second table lists the ten heaviest log entries by render-weight, each by its `log:///<coord>/<op>` handle — the FOLD targets behind the turn weight. The handle carries the turn, so the two tables interlock. {§tokenomics-largest-entries}
1082
1097
  - **Context-window percent.** The headline carries usage as a percent of the ceiling — `usage Y (P%)` — a fullness gauge beside the absolutes. Reads the ceiling already in hand; no extra provider call. {§tokenomics-context-percent}
1083
1098
  - **Depth re-counted at render.** The manifest re-tokenizes each entry's `tokens` through the live provider at build — never the write-time snapshot — so a model change between loops can't stale the catalog. Every token figure in the packet is render-fresh, manifest and budget alike; nothing trusts a cross-loop cached total.
1084
- - **Over-budget is honest.** When usage exceeds the ceiling, `free` floors at 0 and the percent passes 100 the readout shows the overshoot rather than a negative free, so the model knows it's over and curates down. {§tokenomics-over-budget-floor}
1099
+ - **The delivered packet is never over budget.** The readout shows the state of the packet the model actually has, and the grinder (§grinder) folds any over-ceiling packet back under *before* it is sent — so a delivered budget headline is always usage ≤ ceiling, percent 100, free ≥ 0. The percent is of the **post-fold** packet; the pre-fold overshoot is engine trivia the model never sees. A packet that can't be folded under even fully collapsed is the corner case: the loop **hard-413s** rather than deliver an over-budget packet. Its STORED failure record renders the overshoot honestly — `free` floors at 0 (never negative), the percent passes 100 — never clamped to hide the degenerate state, but never the model's reasoning surface either. {§tokenomics-over-budget-floor}
1085
1100
 
1086
1101
  **Rejected / obviated.**
1087
1102
 
@@ -1100,7 +1115,7 @@ Each entry: question, answer, rationale, migration path.
1100
1115
 
1101
1116
  **Tier — session is the world; permissions are the session's.** Membership, the overlay, and the git flags are **session-tier** (`session_constraints.session_id`, service/session config) — never per-run. Every run in a session shares one world (§machine-processes: one filesystem, one overlay); a run is a *log* — a perspective over that world — owning no membership of its own. A declaration reshapes the one world for every run, never per-connection. `runs.origin` is attribution (whose perspective), not a permission.
1102
1117
 
1103
- **Workspace identity.** No `projects` table; `sessions.project_root TEXT` (nullable = headless) anchors the workspace. `entries.scope` unchanged (`∈ {'agent','session'}`). Workspace = session; no users/auth/multi-tenant.
1118
+ **Workspace identity.** No `projects` table; `sessions.project_root TEXT` (nullable = headless) anchors the workspace. `entries.scope {'session','run'}` (agent-scope retired). Workspace = session; no users/auth/multi-tenant.
1104
1119
 
1105
1120
  **git is the substrate.** {§membership-git-membership} git-tracked files (`git ls-files`) are members with no explicit overlay — channel-less markers, disk is truth. git absent → no fs-walk (non-git/headless get no substrate membership); `pick` is then the sole source.
1106
1121
 
@@ -1131,11 +1146,11 @@ The CAS is the **hard backstop**, at the moment of writing, on every accept path
1131
1146
 
1132
1147
  **Rationale.** Session is the right scope unit; membership *is* the curation, outsourced and tiered: git bounds it by tracking, the client supersedes by overlay, the model curates its own render by READ/FOLD — the engine curates nothing. The forest falls out of "session = world": one workspace can be many repos, so membership is their union, declared not guessed (the scan and its security are the client's). Exhaustiveness is a property of *coverage*, not *work*: every member is checked every turn so no drift hides, but unchanged members cost only a detect — the full-repo cost is git's to bound (what it tracks) and the client's to bound (`hide`), never the engine's to pay re-reading what hasn't moved.
1133
1148
 
1134
- **Migration path.** `session_constraints.effect` gains `repo`; the three renames (`add`→`pick`, `ignore`→`hide`, `read-only`→`view`) are wire-surface changes on `session.constrain`. Forest resolution iterates declared repos (was one `ls-files` at root). The change-detect adds a per-member stored signal — mtime+size or content hash, and *that choice is the EMI reliability bound* — gating the existing materialize. `PLURNK_GIT_ALLOWED` replaces `PLURNK_GIT_ENABLED`; `PLURNK_GIT_AUTO` is new. Tenancy / cross-session shared workspaces still require a `workspaces` table lifting constraints off `session_constraints`.
1149
+ **Migration path.** `session_constraints.effect` gains `repo`; the three renames (`add`→`pick`, `ignore`→`hide`, `read-only`→`view`) are wire-surface changes on `session.constrain`. Forest resolution iterates declared repos (was one `ls-files` at root). The change-detect adds a per-member stored signal — mtime+size or content hash, and *that choice is the EMI reliability bound* — gating the existing materialize. `PLURNK_GIT_ALLOWED` (the hard ceiling) and `PLURNK_GIT_AUTO` (the default declaration) are the git flags (§membership-git-flags). Tenancy / cross-session shared workspaces still require a `workspaces` table lifting constraints off `session_constraints`.
1135
1150
 
1136
1151
  ### §grinder Budget enforcement: the grinder
1137
1152
 
1138
- **Question.** §tokenomics surfaces the budget honestly and the model curates against `tokensFree` — almost always enough. Two states defeat self-regulation, neither the model's doing: a jumbo prompt (the turn-0 environment), and an unexpectedly large read. (A jumbo repo is no longer its own case — with no index nothing auto-renders the repo; it surfaces only as a large `manifest.json` READ, which the model chunks like any big read.) What enforces the ceiling when the signal isn't enough?
1153
+ **Question.** §tokenomics surfaces the budget honestly and the model curates against `tokensFree` — almost always enough. Two states defeat self-regulation, neither the model's doing: a jumbo prompt (the turn-0 environment), and an unexpectedly large read. (A jumbo repo is no longer its own case — with no index nothing auto-renders the repo; it surfaces only as a large catalog `FIND`, which the model pages like any big result.) What enforces the ceiling when the signal isn't enough?
1139
1154
 
1140
1155
  **Decision — a pre-LLM grinder, fired only on actual overflow.** In `Engine.runTurn`, after the packet is assembled (`#buildRequestPacket`) and before `provider.generate`, the assembled render-weight (§tokenomics) is measured against the ceiling. At or under → the packet ships untouched; the grinder never trims speculatively or "helpfully." {§grinder-overflow-only} On overflow it reverts the prior turn, then hard-stops if that isn't enough:
1141
1156
 
@@ -1146,7 +1161,7 @@ The CAS is the **hard backstop**, at the moment of writing, on every accept path
1146
1161
 
1147
1162
  **What the model sees.** A `budget_overflow` telemetry event (§telemetry), in the model's own terms: which of its entries left the window, by scheme. No mechanism vocabulary — no "layer," no "grinder," no "reclaim" — and no advice. The engine reports *what happened to the model's world*; the budget readout (§tokenomics) — its turn and entry weights — is the diagnostic surface, and the model — which can see what changed in its repo, its reads, its turn — diagnoses the cause the engine can't attribute. {§grinder-event-model-terms} Per the gamification policy (§telemetry), the *strike* the overflow triggers stays engine-internal; the model sees the hidden entries, never the accounting.
1148
1163
 
1149
- **Rationale.** The model owns curation (§tokenomics); the grinder is the exceptional backstop. It only *folds* — reversibly — the prior turn's render; nothing is deleted, so the model can OPEN it back and log history stays intact. Rummy's §1316 spec described clearing log *bodies*, but its code instead folded the prior turn whole — because body-clearing is destructive (it deletes the read result) and bespoke. The code was the lesson; plurnk follows it.
1164
+ **Rationale.** The model owns curation (§tokenomics); the grinder is the exceptional backstop. It only *folds* — reversibly — the prior turn's render; nothing is deleted, so the model can OPEN it back and log history stays intact. Rummy's own spec described clearing log *bodies*, but its code instead folded the prior turn whole — because body-clearing is destructive (it deletes the read result) and bespoke. The code was the lesson; plurnk follows it.
1150
1165
 
1151
1166
  **Migration path.** None on mechanism. Speculative or non-overflow trimming is a different feature, deliberately excluded — the grinder fires only in response to actual overflow.
1152
1167
 
@@ -1203,7 +1218,7 @@ The CAS is the **hard backstop**, at the moment of writing, on every accept path
1203
1218
 
1204
1219
  ```ts
1205
1220
  type PacketSection = {
1206
- name: string; // stable id: definition, tools, schemes, log, prompt, budget, errors, git, requirements — or a plugin's own
1221
+ name: string; // stable id: definition, tools, schemes, system-policy, project-policy, budget, prompt, errors, log, git, requirements — or a plugin's own
1207
1222
  slot: "system" | "user"; // the prompt-cache boundary; system-slot sections build the cache-stable system message
1208
1223
  header: string | null; // "## Plurnk System X", or null (definition renders verbatim)
1209
1224
  content: string; // rendered markdown — what the model saw
@@ -1226,7 +1241,12 @@ The wire projection (`PacketWire.renderSlot`) groups sections by slot into the s
1226
1241
 
1227
1242
  ### §telemetry user.telemetry — model-facing runtime telemetry
1228
1243
 
1229
- Telemetry the model MUST react to immediately. Errors render in their own `errors` section (§packet-assembly unbundled them from a single telemetry block) — transient, appearing on the turn AFTER the failure, clearing once seen. The `log` section is the durable audit; the `errors` section (rendered from `packet.telemetryErrors`) is the **alert**.
1244
+ The model's runtime alert surface, with two sources by lifetime:
1245
+
1246
+ - **Errors are LOG ITEMS.** A model FAILURE — a parse failure (an actionless `op='error'` row) or an action that returned `status_rx ≥ 400` — is a durable `log_entries` row. It folds, kills, and budgets like any log entry (§open-fold), so the model recalls or curates its own mistakes and the grinder's prior-turn rollback can reclaim them — ONE budget surface, the log. The `errors` section is a derived POINTER INDEX over the recent `status_rx ≥ 400` rows (status + `log:///<coord>`), aiming the model at them; it holds no bodies and no fold/budget state of its own. Durable: an error persists until the model folds or kills it, not "cleared once seen."
1247
+ - **Engine NOTICES are telemetry.** Ephemeral events the engine emits while steering or narrating — a provider's `grammar_unenforced`, a `max_commands_exceeded` truncation, a `budget_overflow` fold, the premature-terminate/idle steers. Transient: they appear on the turn AFTER the event and drain once seen (`packet.telemetryErrors` is their only home). They are NOT the model's failures.
1248
+
1249
+ The `log` section is the durable audit; the `errors` section surfaces both — the error pointers (durable, in the log) and the notices (ephemeral).
1230
1250
 
1231
1251
  **Grammar contract:**
1232
1252
 
@@ -1236,27 +1256,25 @@ Telemetry the model MUST react to immediately. Errors render in their own `error
1236
1256
  **Plurnk-service rendering:**
1237
1257
 
1238
1258
  - `budget` per §tokenomics: turn-weight and heaviest-entries tables with `tokenCeiling`/`tokenUsage`/`tokensFree`.
1239
- - `errors[]` from previous turn's dispatch. Required: `kind` discriminator. Additional kind-specific fields are flat on the element NO nested `detail`. Canonical-JSON serialization sorts keys for prefix-cache friendliness.
1240
- - Wire: one `* {canonical-JSON}` line per error under `## Plurnk System Errors`, push order. Buffer drains on read. {§telemetry-drain-on-read}
1259
+ - **Error pointers** — derived from the previous turn's `log_entries WHERE status_rx ≥ 400`: `kind: "action_failure"`, `coordinate` (`<L>/<T>/<S>`), `op` (`error` for a parse failure, the real op for a failed action), `status`, `target`, and a terse `error`. The full body (message, snippet) lives on the foldable log row, not here.
1260
+ - **Notices** one `* {canonical-JSON}` line per event under `## Plurnk System Errors`, push order; flat kind-specific fields (NO nested `detail`); canonical-JSON sorts keys for prefix-cache friendliness. The notice buffer drains on read — each appears on exactly one packet. {§telemetry-drain-on-read}
1241
1261
  - **No prose `message` field.** Errors carry structured facts. The `kind` is the alert; the named fields are the data. Guidance, advice, hints, and exhortation MUST NOT appear in telemetry. Letting the model infer what to do from facts (and the log) beats handing it instructions it will second-guess.
1242
1262
  - **Gamification policy (rummy precedent, plugins/error/error.js).** The model sees errors that **happened** — its actions failed, its emission didn't parse, its ops were truncated. The model does NOT see the engine's accounting *about* errors: strike streaks, cycle detection, sudden-death thresholds, no-ops bookkeeping. Surfacing internal state creates a gamification surface where the model optimizes for engine metrics (manufacturing a clean turn to reset the strike counter, e.g.) instead of the task. Engine bookkeeping drives abandonment silently; the model just sees its actual failures.
1243
1263
 
1244
- **Kinds emitted by plurnk-service:**
1264
+ **The error pointer + the engine NOTICE kinds:**
1245
1265
 
1246
1266
  | `kind` | Source | Required fields |
1247
1267
  |---|---|---|
1248
- | `parse_error` | Grammar parser failed mid-statement | `source: "grammar"`, `kind`, `message`, `position` (content-offset), `snippet` (model's offending line, N:\t-prefixed), `parserSource` (`lexer`/`parser`/`visitor`) |
1249
- | `action_failure` | Log entry with `status_rx 400` from previous turn | `kind`, `coordinate` (`<L>/<T>/<S>`), `op`, `status`, `target` (URI or null). May carry scheme-emitted `error` (a terse fact, not guidance). |
1268
+ | `action_failure` | The DERIVED error pointer any `log_entries` row with `status_rx ≥ 400` from the previous turn: a parse failure's actionless `op='error'` row, or a failed action | `kind`, `coordinate` (`<L>/<T>/<S>`), `op`, `status`, `target` (URI or null). May carry a terse `error` fact. The full message/snippet lives on the foldable log row. |
1269
+ | `grammar_unenforced` | (provider, forwarded) GBNF-filter divergence the model's bytes diverged from the transported grammar | `source: "provider:*"`, `kind`, `message`, `position` (content-offset), `snippet` |
1250
1270
  | `max_commands_exceeded` | Single emission exceeded `PLURNK_MAX_COMMANDS` cap; overflow ops dropped without dispatch | `source: "engine:rail"`, `kind`, `emitted`, `dropped` |
1251
1271
  | `budget_overflow` | Assembled packet exceeded the budget ceiling; entries moved out of the window to fit | `source: "engine:rail"`, `kind`, `hidden` (per-scheme `[{scheme, count}]` — entries removed from the window) |
1252
1272
 
1253
- Strike accounting, cycle detection, sudden-death thresholds, and no-ops bookkeeping are all engine-internal — they drive abandonment silently per the gamification policy above. Action-bound failures (handler returned 4xx/5xx or threw) mirror as `action_failure` kind on the next packet. Full detail queryable via `log:///`. {§telemetry-no-error-scheme}
1254
-
1255
- **No `error://` scheme.** Actionless failures route to telemetry, not a queryable scheme namespace.
1273
+ Strike accounting, cycle detection, sudden-death thresholds, and no-ops bookkeeping stay engine-internal — they drive abandonment silently per the gamification policy. Both error kinds a failed action AND an actionless parse failure — are LOG ITEMS (`log:///<coord>`, `op='error'` for the latter, `status_rx ≥ 400`), foldable and re-OPENable, with full detail (message, snippet) on the row. The `errors` section surfaces a derived pointer to each. There is **no bespoke `error://` scheme**: errors live in the log, addressable + curatable like any row — not a separate queryable namespace. {§telemetry-no-error-scheme}
1256
1274
 
1257
- **Client surface: `telemetry/event` notification.** Every event the engine pushes to the loop's telemetry buffer also broadcasts live via the `telemetry/event` WS notification. Same envelope on both sides `{ source, kind, message?, position?, …kind-specific }` per the grammar's `TelemetryEvent` schema. The model sees the event on the NEXT packet's `errors` section (drains on read); the client sees it the moment it lands. Client uses cases: render parse errors in a debug panel (the `snippet` field is content the model emitted), surface strike/sudden_death as "loop is degrading" toasts, log everything to a session timeline. Scoped to the loop's session. {§telemetry-telemetry-event-notify}
1275
+ **Client surface.** Engine NOTICES broadcast live via the `telemetry/event` WS notification same envelope as the model's drained copy (`{ source, kind, message?, position?, …kind-specific }` per the grammar's `TelemetryEvent` schema), the moment they land, scoped to the loop's session (a `grammar_unenforced` snippet in a debug panel, a session timeline). ERRORS do not broadcast on this surface: they are log rows, and the client reads them the same way the model curates them `log.read` / the `log/entry` notification, the durable log. {§telemetry-telemetry-event-notify}
1258
1276
 
1259
- **Content-offset snippet rendering.** When telemetry carries `position: { type: "content-offset", line, column }`, plurnk-service extracts a ±N-line slice from the model's own prior `assistant.content` and renders it as an `N:\t`-prefixed heredoc under an `error://<line>` fence, immediately following the event meta line. Without the snippet, the model gets "invalid xpath at 1:0" with no way to trace what it wrote at 1:0 — and tends to regenerate the same broken emission. With it, recovery is direct (canonical case: the edit-todo demo where a READ body starting with `//` got xpath-dispatched). The snippet field is stripped from the meta JSON so it appears once, in the body block. {§telemetry-content-offset-snippet}
1277
+ **Content-offset snippet rendering.** When a NOTICE carries `position: { type: "content-offset", line, column }` (e.g. a provider's `grammar_unenforced`), plurnk-service extracts a ±N-line slice from the model's own prior `assistant.content` and renders it as an `N:\t`-prefixed heredoc under an `error://<line>` fence, immediately following the event meta line. Without the snippet, the model gets "invalid xpath at 1:0" with no way to trace what it wrote at 1:0 — and tends to regenerate the same broken emission. With it, recovery is direct. The snippet field is stripped from the meta JSON so it appears once, in the body block. (A parse-error LOG ROW carries the equivalent snippet in its own foldable body — the same locator, durable in the log.) {§telemetry-content-offset-snippet}
1260
1278
 
1261
1279
  ### §tools user.tools — the capability sheet
1262
1280
 
@@ -1270,7 +1288,11 @@ A `## Plurnk System Schemes` section renders in the system slot **after the defi
1270
1288
 
1271
1289
  ### §inject system.inject — the operator injection
1272
1290
 
1273
- When `PLURNK_PACKET_INJECT` names a readable markdown file, its content renders as a `## Plurnk Operator Notes` section in the system slot **right after the teaching** (definition → tools → schemes → inject), before the log — part of the cached prefix. Read per-turn so the operator's edits take effect live; a set-but-unreadable path fails the turn hard (a deliberate setting with a broken path is a misconfig, surfaced not hidden). `~/` expands to home. It's the operator-side complement to the plugin section hook — a pressure valve so reshaping the packet edits operator content, never the core. Unset → no section. {§packet-inject}
1291
+ When `PLURNK_PACKET_INJECT` names a readable markdown file, its content renders as a `## Plurnk Operator Notes` section in the system slot **right after the teaching** (definition → tools → schemes → inject), ahead of the policy sections and budget — part of the cached prefix. Read per-turn so the operator's edits take effect live; a set-but-unreadable path fails the turn hard (a deliberate setting with a broken path is a misconfig, surfaced not hidden). `~/` expands to home. It's the operator-side complement to the plugin section hook — a pressure valve so reshaping the packet edits operator content, never the core. Unset → no section. {§packet-inject}
1292
+
1293
+ ### §policy system.policy — the client's policy injection
1294
+
1295
+ Two sections ride the system slot **below the operator notes, above budget**: `## Plurnk System Policy` from `PLURNK_POLICY` (default `~/.plurnk/AGENTS.md`) and `## Project Policy` from `PLURNK_PROJECT` (default `<projectRoot>/AGENTS.md`, resolved relative to the session root). AGENTS.md is **policy** — the client's authoritative rules promoted into the privileged zone — NOT a curatable, foldable, READ-able entry; the model cannot FOLD it away. A default-absent path is silent (the section is omitted); an explicit override (env set) that fails to read fails the turn hard — a deliberate setting with a broken path is a misconfig, surfaced not hidden. Read per-turn so edits take effect live. Reference/scratch docs are NOT policy — they ride `PLURNK_MD_*` (materialized as READ-able entries, §operator-config), which is where the dev-notes AGENTS.md used to hold belong. {§policy-sections}
1274
1296
 
1275
1297
  **The scheme self-doc contract.** `example` is the hot-path one-liner; `documentation` is the deep doc — the exact shape execs already use (`example` + `documentation`). `SchemeRegistry.teach()` renders the directory; `docEntries()` materializes the docs (per loop.run, alongside the operator docs). `documentation` rides a service-side `SchemeManifest` extension until plurnk-schemes#25 lands it in the contract.
1276
1298
 
@@ -1288,42 +1310,38 @@ Rendered at the END of the user packet under `## Plurnk System Requirements` {§
1288
1310
 
1289
1311
  Body matchers and `<L>` both dispatch on entry mimetype. Body matcher: leading-char classification (`//` xpath, `/` regex, `$` jsonpath, otherwise glob). `<L>`: line-navigable → by line, structured → by item.
1290
1312
 
1291
- ### §matcher-dispatch Matcher dispatch (delegated to `Mimetypes.query`)
1313
+ ### §matcher-dispatch Matcher dispatch (service-owned, over daughter primitives)
1292
1314
 
1293
- `matchAgainstContent` (exported from `@plurnk/plurnk-schemes`) is an adapter over `Mimetypes.query(input, expression)`. Framework parses leading prefix, resolves per-mimetype handler, returns `QueryMatch[]`. Adapter maps typed errors:
1315
+ `Matcher.matchAgainstContent` (in-tree, `src/content/matcher.ts`) is the **service's own** dialect dispatch — `Mimetypes.query` is NOT consumed (§mimetype-methods). It switches on the matcher's dialect and calls the daughter's individual primitives: `glob → queryGlob` and `regex → queryRegex` over the raw content; `jsonpath → queryJsonpathObject` over the `deepJson` projection and `xpath → queryXpathString` over `deepXml` (both pulled from `mimetypes.process({channels})`, so a structural dialect works over any source type). `~semantic` is service-side and parked; `@graph` resolves over the service's own symbol indexes (§mimetype-methods). Each returns `QueryMatch[]`, rendered as `<source-line>:\t<value>`. Status mapping:
1294
1316
 
1295
- | Framework error | HTTP status |
1317
+ | Result | HTTP status |
1296
1318
  |---|---|
1297
- | `UnsupportedDialectError` | 415 |
1298
- | `InvalidExpressionError` | 400 |
1299
- | `QueryParseFailureError` | 203 (soft fallback: raw content as `text/markdown` with `reason`) |
1300
- | Empty match array | 204 |
1301
1319
  | Match array | 200 |
1320
+ | Empty match array | 204 |
1321
+ | Malformed matcher expression | 400 |
1322
+ | Source unparseable for its mimetype | 203 (soft fallback: raw content as text with `reason`) |
1323
+ | `~semantic` (parked, needs vector design) | 501 |
1302
1324
 
1303
1325
  203 is HTTP-creative ("Non-Authoritative Information"). On parse failure, returns raw bytes as text primitive with `reason` so the model can fall back to regex/visual parsing or fix source. {§matcher-dispatch-203-soft-fallback}
1304
1326
 
1305
1327
  Glob anchoring (`TODO*` starts-with, `*TODO*` contains, `*.log` ends-with, `[Tt]odo*` char class) lives in framework's `BaseHandler`.
1306
1328
 
1307
- ### §matcher-result Matcher result shape (uniform across dialects)
1329
+ ### §matcher-result Matcher result shape READ returns matching LINES, uniformly
1308
1330
 
1309
- Body: one match per line as `<line>:\t<value>` — the same `N:\t` form READ emits, so `<L>` can page the result set. Empty 204. Mimetype = `text/markdown` regardless of source dialect.
1331
+ The contract is the grammar's: **plurnk.md §"`<Line> / <Result>`""FIND returns rows of results, READ returns lines of content"**, and READ "prefixes every line with line numbers and a hard tab, `N:\t`" (one source-line number, not part of the source). This section documents the service's implementation of that.
1310
1332
 
1311
- - `<line>`1-indexed source line, shifted back to source coordinates when matching inside an `<L>` slice.
1312
- - `<value>` — the extracted match, rendered bare when it is a single-line string, else JSON-encoded (preserving the one-match-per-line invariant). Polymorphic per dialect:
1313
- - **bare regex** → string (full match)
1314
- - **anon captures** → array `[c1, c2, …]`
1315
- - **named captures** → object `{name: v, …}`. Mixed anon+named uses positional keys `"1"`, `"2"` alongside names.
1316
- - **glob** → string (matching source line)
1317
- - **jsonpath** → JSON value at the path
1318
- - **xpath text/attr** → string
1319
- - **xpath node** → serialized XML
1333
+ **A matcher selects locations; it never extracts a value.** Every dialect identifies *where* in the source it matches; READ returns the **source line(s)** at those locations, faithfully one shape for every dialect: `<line>:\t<line-content>`, prefixed with the single source-line number per plurnk.md (shifted back to source coordinates inside an `<L>` slice), never double-numbered. Empty → 204; mimetype `text/markdown` regardless of source. The model reads the line and adapts whatever it needs out of it — READ never pre-chews a match down to a bare value. {§matcher-result-read-returns-lines}
1320
1334
 
1321
- | Dialect | Extracts | Natural use |
1335
+ | Dialect | Selects | Natural use |
1322
1336
  |---|---|---|
1323
- | regex `/pat/` | substring (or captures) | extract the value after X: |
1324
- | glob `pat` | whole matching lines | show lines containing TODO |
1325
- | jsonpath `$.path` | JSON values (parsed value for JSON-shaped mimetypes; bare-leaves outline for markdown/HTML/source) | get the host field / jump to Installation |
1326
- | xpath `//sel` | XML nodes/text/attrs (text/html only) | get the h1 contents |
1337
+ | regex `/pat/` | the lines the pattern occurs in (it *matches*, never captures-and-extracts) | the lines mentioning X |
1338
+ | glob `pat` | the lines the glob matches | the lines containing TODO |
1339
+ | jsonpath `$.path` | the line(s) where the structural path resolves | the line defining `host` |
1340
+ | xpath `//sel` | the line(s) of the selected node (text/html) | the line(s) of the h1 |
1341
+
1342
+ Across a **multi-file target** (a glob over `(target)`), READ returns **one log item per file that contains a match**, each holding that file's matching lines — READ is the content retrieval over the whole matched set, the companion to FIND's survey (§find-result-catalog-rows). *(Engine-level fan-out; tagged + tested when built.)*
1343
+
1344
+ > **Implementation requirement (a mimetypes-daughter need, not a contract exception):** structural dialects must report the SOURCE LINE of each hit. regex/glob match over raw content (the line is in hand); jsonpath/xpath run over the parsed `deepJson`/`deepXml` projection and today return the value — the match primitive must instead carry the line span of each hit, so READ can return the line.
1327
1345
 
1328
1346
  ### §slice-semantics `<L>` semantics by source mimetype
1329
1347
 
@@ -1421,7 +1439,7 @@ Carried from the contract walk; durable.
1421
1439
  - **EDIT `<L>` on non-existent entry** → body becomes content; `<L>` is positional-only on existing content.
1422
1440
  - **COPY `<L>`** → source range, symmetric with READ `<L>`.
1423
1441
  - **READ rx** prefixes each line with `N:\t` per §render-rule. `sliceLinesRaw` (used by COPY) returns the lines without prefix.
1424
- - **FIND body matcher** applies to entry content (all dialects), per-candidate via `Matcher.matchAgainstContent` `Mimetypes.query` (status 200 = content hit → entry selected). Scope + tags select candidates in SQL; the path-glob is the (target).
1442
+ - **FIND body matcher** applies to entry content (all dialects), per-candidate via the in-tree `Matcher.matchAgainstContent` (§matcher-dispatch; status 200 = content hit → entry selected). Scope + tags select candidates in SQL; the path-glob is the (target).
1425
1443
  - **OPEN/FOLD** operate on the **log** (`log:///`), not entries (§open-fold) — FOLD collapses a log row to its path, OPEN restores its body. Aimed at an entry scheme they return 501.
1426
1444
  - **SEND[410]** deletes as a side-effect (not the model idiom; §move): with `#fragment`, that channel only; without, the whole entry. **SEND[499]** is owned by the streaming scheme that holds the subscription.
1427
1445
  - **File scheme** reads disk content with mimetype detected via `Mimetypes.detect({ path })` (plumbed through `PlurnkSchemeContext.mimetypes`). Binary mimetypes → 415 on READ and EDIT.