@plurnk/plurnk-service 0.45.0 → 0.49.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 (79) hide show
  1. package/.env.example +33 -3
  2. package/README.md +1 -1
  3. package/SPEC.md +12 -8
  4. package/dist/core/Engine.d.ts.map +1 -1
  5. package/dist/core/Engine.js +61 -46
  6. package/dist/core/Engine.js.map +1 -1
  7. package/dist/core/Engine.sql +16 -0
  8. package/dist/core/ExecutorRegistry.d.ts +3 -0
  9. package/dist/core/ExecutorRegistry.d.ts.map +1 -1
  10. package/dist/core/ExecutorRegistry.js +7 -2
  11. package/dist/core/ExecutorRegistry.js.map +1 -1
  12. package/dist/core/SchemeRegistry.d.ts +3 -0
  13. package/dist/core/SchemeRegistry.d.ts.map +1 -1
  14. package/dist/core/SchemeRegistry.js +76 -6
  15. package/dist/core/SchemeRegistry.js.map +1 -1
  16. package/dist/core/packet-inject.d.ts +3 -0
  17. package/dist/core/packet-inject.d.ts.map +1 -0
  18. package/dist/core/packet-inject.js +16 -0
  19. package/dist/core/packet-inject.js.map +1 -0
  20. package/dist/core/packet-wire.d.ts +1 -0
  21. package/dist/core/packet-wire.d.ts.map +1 -1
  22. package/dist/core/packet-wire.js +11 -0
  23. package/dist/core/packet-wire.js.map +1 -1
  24. package/dist/core/scheme-types.d.ts +1 -0
  25. package/dist/core/scheme-types.d.ts.map +1 -1
  26. package/dist/core/teaching.d.ts +3 -0
  27. package/dist/core/teaching.d.ts.map +1 -0
  28. package/dist/core/teaching.js +11 -0
  29. package/dist/core/teaching.js.map +1 -0
  30. package/dist/schemes/Exec.d.ts +2 -2
  31. package/dist/schemes/Exec.d.ts.map +1 -1
  32. package/dist/schemes/Exec.js +40 -29
  33. package/dist/schemes/Exec.js.map +1 -1
  34. package/dist/schemes/ExecOutputScheme.d.ts +20 -0
  35. package/dist/schemes/ExecOutputScheme.d.ts.map +1 -0
  36. package/dist/schemes/ExecOutputScheme.js +38 -0
  37. package/dist/schemes/ExecOutputScheme.js.map +1 -0
  38. package/dist/schemes/Known.d.ts.map +1 -1
  39. package/dist/schemes/Known.js +0 -1
  40. package/dist/schemes/Known.js.map +1 -1
  41. package/dist/schemes/Log.d.ts.map +1 -1
  42. package/dist/schemes/Log.js +0 -1
  43. package/dist/schemes/Log.js.map +1 -1
  44. package/dist/schemes/Run.d.ts.map +1 -1
  45. package/dist/schemes/Run.js +0 -1
  46. package/dist/schemes/Run.js.map +1 -1
  47. package/dist/schemes/Unknown.d.ts.map +1 -1
  48. package/dist/schemes/Unknown.js +0 -1
  49. package/dist/schemes/Unknown.js.map +1 -1
  50. package/dist/schemes/_entry-manifest.d.ts.map +1 -1
  51. package/dist/schemes/_entry-manifest.js +7 -2
  52. package/dist/schemes/_entry-manifest.js.map +1 -1
  53. package/dist/schemes/_entry-semantic.d.ts.map +1 -1
  54. package/dist/schemes/_entry-semantic.js +14 -7
  55. package/dist/schemes/_entry-semantic.js.map +1 -1
  56. package/dist/schemes/_entry-semantic.sql +16 -0
  57. package/dist/schemes/exec-abort.d.ts +11 -0
  58. package/dist/schemes/exec-abort.d.ts.map +1 -0
  59. package/dist/schemes/exec-abort.js +23 -0
  60. package/dist/schemes/exec-abort.js.map +1 -0
  61. package/dist/schemes/exec-receipt.d.ts +4 -0
  62. package/dist/schemes/exec-receipt.d.ts.map +1 -0
  63. package/dist/schemes/exec-receipt.js +25 -0
  64. package/dist/schemes/exec-receipt.js.map +1 -0
  65. package/dist/server/Daemon.d.ts.map +1 -1
  66. package/dist/server/Daemon.js +62 -42
  67. package/dist/server/Daemon.js.map +1 -1
  68. package/dist/server/drain.sql +10 -0
  69. package/dist/server/methods/loop_run.d.ts.map +1 -1
  70. package/dist/server/methods/loop_run.js +31 -11
  71. package/dist/server/methods/loop_run.js.map +1 -1
  72. package/dist/service.js +1 -1
  73. package/dist/service.js.map +1 -1
  74. package/docs/known.md +3 -0
  75. package/docs/log.md +25 -0
  76. package/docs/run.md +3 -0
  77. package/docs/unknown.md +3 -0
  78. package/migrations/0000-00-00.01_schema.sql +2 -2
  79. package/package.json +13 -12
package/.env.example CHANGED
@@ -32,6 +32,10 @@ PLURNK_DB_PATH=~/.plurnk/plurnk.db
32
32
  PLURNK_HOST=127.0.0.1
33
33
  PLURNK_PORT=3044
34
34
 
35
+ # --- Client (read by the `plurnk` CLI from this shared home; the daemon ignores it) ---
36
+ # The daemon WebSocket URL the client connects to (mirrors PLURNK_HOST/PORT above).
37
+ # PLURNK_WS=ws://127.0.0.1:3044
38
+
35
39
  # --- Model aliases ---
36
40
  # PLURNK_MODEL is the active provider for every loop (a client may override per loop
37
41
  # via loop.run({alias})). Out of the box it is `plurnk`. Change this one line to use
@@ -64,7 +68,12 @@ PLURNK_RPC_TIMEOUT=30000
64
68
  # so a fast exec's output can land in it instead of a turn later. A fixed grace
65
69
  # beat, NOT a wait-for-completion (slow execs proceed + surface via the wake path).
66
70
  # 0 = off (the model sees fast-exec output a turn late, as today).
67
- PLURNK_EXEC_WAIT_MS=0
71
+ PLURNK_EXEC_WAIT_MS=1000
72
+ # Teardown reap grace: when a loop/run tears down a background exec, the spawn gets a polite
73
+ # signal first (SIGHUP, or the model's KILL[code]); a stream that IGNORES it is hard-killed
74
+ # (SIGKILL, to the whole process group) this many ms later — so the reap can't wedge on a
75
+ # signal-ignoring child. plurnk-execs refuses to bake this number; the consumer owns it.
76
+ PLURNK_EXEC_KILL_GRACE_MS=2000
68
77
  PLURNK_LOOP_TIMEOUT=86400000
69
78
 
70
79
  # --- Engine rails ---
@@ -88,7 +97,7 @@ PLURNK_GIT_ALLOWED=1
88
97
  # repos explicitly via the `repo` overlay. SPEC §membership forest.
89
98
  PLURNK_GIT_AUTO=1
90
99
 
91
- # --- Reference docs (auto-READ at turn 0) ---
100
+ # --- Reference docs (auto-READ at turn 1) ---
92
101
  # PLURNK_MD_<ALIAS>=<path> materializes <path>'s markdown as a plurnk://<ALIAS>.md
93
102
  # entry the model READs at turn 0 — an idiomatic way to inject standing context
94
103
  # (an ordinary entry + READ op, not a bespoke packet section). ~ expands to home;
@@ -96,7 +105,14 @@ PLURNK_GIT_AUTO=1
96
105
  # per session via session.create settings.mdDocs (content, not a path); those UNION
97
106
  # with these env docs, keyed by alias — the client wins a collision (#231).
98
107
  # Commented out = no docs by default.
99
- # PLURNK_MD_POLICY=~/.plurnk/AGENTS.md
108
+ PLURNK_MD_POLICY=~/.plurnk/AGENTS.md
109
+
110
+ # --- AGENTS.md (auto-READ at turn 1) ---
111
+ # Scan project root and if there's an agent file, pick it into the manifest and
112
+ # read its entire content into turn 1. Accepts multiple.
113
+ # PLURNK_AGENTS_FILES="AGENTS.md,CLAUDE.md"
114
+ PLURNK_AGENTS_FILES="AGENTS.md"
115
+ PLURNK_AGENTS_AUTO=1
100
116
 
101
117
  # Turn-0 manifest preview: foist a READ of plurnk://manifest.json into the model's
102
118
  # first turn so a run opens with the session catalog, not blank. -1 = the full
@@ -141,6 +157,10 @@ PLURNK_VERSION_POLL_TTL=3600000
141
157
  # @plurnk/plurnk-grammar; an absolute/relative path is your own. =0 (or empty) disables.
142
158
  # PLURNK_PROVIDERS_GBNF=plurnk.gbnf
143
159
 
160
+ # GBNF debug, honored natively by @plurnk/plurnk-providers (>=0.13.0): validate the grammar locally
161
+ # (fail-hard if malformed) and run UNCONSTRAINED, never sending it to the model. Dev aid; off by default.
162
+ PLURNK_GBNF_DEBUG=0
163
+
144
164
  # Provider retry attempts (providers 0.7+): how many times generate() retries a
145
165
  # TRANSIENT failure (429 rate-limit, 5xx/network) with exponential backoff (base 2s
146
166
  # is a provider constant — the COUNT is the operator knob). REQUIRED, fail-hard if
@@ -163,6 +183,16 @@ PLURNK_PROVIDER_RETRY_ATTEMPTS=3
163
183
  # for "on, zero third-party". Example: =acme-execs-cobol,@firewolf/firepad
164
184
  PLURNK_PLUGINS_TRUSTED_ONLY=0
165
185
 
186
+ # PLURNK_DOCS_EXCLUDE — comma list of scheme/exec names dropped from BOTH the teaching oneliner
187
+ # and the materialized pull-doc on load. The self-evident (plurnk/file) + retired (exec) names the
188
+ # model needs no doc for. Empty → exclude nothing. Unknown names are inert (a filter, not a contract).
189
+ PLURNK_DOCS_EXCLUDE="plurnk,file,exec"
190
+
191
+ # PLURNK_PACKET_INJECT — an operator markdown file injected as a section right after the teaching
192
+ # (the cached prefix). Read per-turn (live edits); a set-but-unreadable path fails the turn HARD.
193
+ # `~/` expands to home. Unset → no section. The pressure valve for "improve the packet" without a fork.
194
+ # PLURNK_PACKET_INJECT="~/injection.md"
195
+
166
196
  # --- Semantic search (~query chunking) ---
167
197
  # Project Semantics tiles each entry into <=window chunks so a large body is fully
168
198
  # searchable, not truncated. ACTIVE only when the installed embedder reports its
package/README.md CHANGED
@@ -41,7 +41,7 @@ Config + state live in `~/.plurnk/` (created on first run): set `PLURNK_MODEL` a
41
41
 
42
42
  ## Semantic search
43
43
 
44
- `FIND` ranks via an optional embedder peer, `@plurnk/plurnk-mimetypes-embeddings` (heavy native deps; not installed by default). Absent → `FIND` degrades and `start` prints an `embedder inactive` notice. Enable: `npm i @plurnk/plurnk-mimetypes-embeddings`.
44
+ `FIND`'s `~query` ranks semantically via an optional embedder peer, `@plurnk/plurnk-mimetypes-embeddings` (heavy native deps; not installed by default). Absent → `~query` falls back to FTS keyword ranking and `start` prints an `embedder inactive` notice. Enable vector search: `npm i @plurnk/plurnk-mimetypes-embeddings`.
45
45
 
46
46
  ## Tests
47
47
 
package/SPEC.md CHANGED
@@ -193,10 +193,10 @@ Beyond the three creation ops:
193
193
  A run is a **log plus a cancellation scope** — one `AbortController` per run, reused while live and replaced only once aborted, so a cancel ends the run as a unit and a later `loop.run` is never born cancelled. A run's queued loops are advanced by a **drain**: a single per-run worker that claims loops atomically (status 100→102) and runs each under the run's scope. A loop may spawn **streams** (execs) that outlive it; each is a row in the subscription registry (§subscriptions) — the durable record of what the run holds open. Cancellation and conclusion are defined against these structures, never wall-clock timing.
194
194
 
195
195
  - **One drain advances a run.** At most one drain is registered for a run at any instant: a `loop.run` or wake on a run with a live drain folds in (active→next-turn) or enqueues a loop that drain claims, never a second parallel drain. A drain's start and its empty-queue teardown relinquish run under one per-run lock, so the teardown's re-claim cannot race a concurrent start into a double-drain. {§run-lifecycle-single-drain}
196
- - **A cancel reaps every stream the run holds — by the registry.** `loop.cancel` / `KILL` / shutdown abort the run scope AND iterate the run's open subscriptions, aborting each via its owning scheme; the registry is the source of truth, the in-process abort signal a fast-path optimization. A stream that is running, mid-spawn (its row written before it is killable), or spawned after the cancel is reaped alike. {§run-lifecycle-total-reap}
196
+ - **A cancel reaps every stream the run holds — by the registry.** `loop.cancel` / `KILL` / shutdown abort the run scope AND iterate the run's open subscriptions, aborting each via its owning scheme; the registry is the source of truth, the in-process abort signal a fast-path optimization. A stream that is running, mid-spawn (its row written before it is killable), or spawned after the cancel is reaped alike. The teardown abort is a BOUNDED reap — the executor sends a polite signal then SIGKILL after a consumer-set grace (`PLURNK_EXEC_KILL_GRACE_MS`), so a signal-ignoring stream can't wedge it; a model `KILL[code]` on a live stream instead delivers exactly that signal once (bare `KILL` → the executor's SIGHUP default, `KILL[9]` → SIGKILL), the model owning any escalation. {§run-lifecycle-total-reap}
197
197
  - **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
198
  - **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
- - **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 summary into its next turn; a dormant run opens a fresh loop with the summary as its prompta long-running result is never lost because its spawning loop ended first. {§run-lifecycle-wake-liveness}
199
+ - **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}
200
200
  - **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
201
 
202
202
  ---
@@ -561,7 +561,7 @@ Engine routes unconditionally to `exec` scheme (path slot is `cwd`, not a URI).
561
561
 
562
562
  **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}
563
563
 
564
- 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** in-process: no proposal, no human gate, no notification. The run is awaited synchronously and its channel content rides back as the EXEC result body the same turn not streamed to the entry for a next-turn read. {§exec-readpure-inline}
564
+ 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** in-process: no proposal, no human gate, no notification. The run is awaited synchronously and its output's RECEIPT — the `<tag>:///<coord>` address + a structural `OrientIndex` (counts/shape/keys/headings, never the content) rides back as the EXEC result body the same turn; the model READs the address to pull content. Receipt-only (read/pure included) is the containment that keeps tool output off the packet — #240. {§exec-readpure-inline}
565
565
 
566
566
  `SEND[499](exec:///<runtime>/<loop>/<turn>/<seq>)` cancels in-flight subprocess via subscription registry's stored AbortController (§stream-control).
567
567
 
@@ -896,7 +896,7 @@ registry.register("loop.run", {
896
896
 
897
897
  | Method | Params | Result | Notes |
898
898
  |-------------------|-------------------------------------|------------------------|-------|
899
- | `loop.run` | `prompt: string`, `maxTurns?: number`, `alias?: string`, `flags?: LoopFlags` | `{ loopId, turnIds, finalStatus, hitMaxTurns, reason }` | Model-driven loop. Optional `alias` overrides the boot-time `PLURNK_MODEL`. Optional `flags` carries per-loop flags (currently `{yolo?: boolean}`; more arrive as wired — see §engine-rails). Streams `log/entry` and `loop/proposal` notifications during. `longRunning: true`. {§methods-loop-run} |
899
+ | `loop.run` | `prompt: string`, `maxTurns?: number`, `alias?: string`, `flags?: LoopFlags` | `{ loopId, action, finalStatus: 100 }` | Model-driven loop. **Accepts and returns immediately** (`finalStatus: 100`; `action` = `enqueued_new_loop` \| `injected_next_turn`) — it never blocks on the loop, which can park indefinitely (`SEND[202]`, §run-lifecycle-wake-liveness). The loop's outcome — `finalStatus`, `turnIds`, `hitMaxTurns`, `usage` — arrives on the **`loop/terminated`** event. Optional `alias` overrides the boot-time `PLURNK_MODEL`. Optional `flags` carries per-loop flags (`{yolo?: boolean}`; more as wired — see §engine-rails). Streams `log/entry` and `loop/proposal` during. `longRunning: false`. {§methods-loop-run} |
900
900
  | `loop.resolve` | `logEntryId: number`, `decision: "accept" \| "reject" \| "cancel"`, `body?: string`, `outcome?: string` | `{ status, logEntryId }` | Resolve a pending proposal (status=202 log entry). Engine.dispatch unpauses on resolution. |
901
901
  | `loop.cancel` | `reason?: string` | `{ cancelled, runId, reason }` | Abort the attached run's active drain. `{cancelled: true}` if a drain was running, `{false}` if idle. Cancelled loops close at 499; queued-but-unclaimed loops stay enqueued. Default reason `user_cancelled`. {§methods-loop-cancel} |
902
902
  | `providers.list` | none | `{ aliases: ProviderAlias[] }` | Lists configured `PLURNK_MODEL_<alias>` entries with `{alias, provider, model, active}`. Clients use to populate model-selection UI. |
@@ -947,7 +947,7 @@ Server-initiated events on the same WebSocket.
947
947
  | `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). |
948
948
  | `session/created` | `{ id, name, projectRoot }` | Any client creates a session. |
949
949
  | `stream/event` | `{ entryId, channel, state, contentLength }` | Channel content grows or state transitions. {§notifications-stream-event-on-channel-change} |
950
- | `stream/concluded` | `{ entryId, target, subscriptionId, scheme, closeStatus, summary, wakeAction, wakeLoopId? }` | A streaming subscription closed (subprocess finished / errored / cancelled). `wakeAction` says whether the daemon opened a fresh loop to surface the conclusion to the model. {§notifications-stream-concluded} |
950
+ | `stream/concluded` | `{ entryId, target, subscriptionId, scheme, closeStatus, summary, wakeAction, wakeLoopId? }` | A streaming subscription closed (subprocess finished / errored / cancelled). `wakeAction` says how the conclusion reached the run: `resumed-loop` (a slept `202` loop resumed in place, §run-lifecycle-wake-liveness), `no-op-active-loop` (folded into a live loop's next turn), `skipped-aborted`/`skipped-cancelled`/`skipped-no-provider`, or `no-loop` (nothing to resume). `summary` rides the notification for client display; it is no longer fed to the model as a prompt. {§notifications-stream-concluded} |
951
951
  | `telemetry/event` | `{ loopId, event: TelemetryEvent }` | A TelemetryEvent (parse error, engine-rail strike/cycle/sudden-death, scheme/provider failure) was buffered — the same envelope the model sees on the next packet, delivered live for client surfacing. {§notifications-telemetry-event} |
952
952
 
953
953
  `stream/event` carries metadata only, never content. Clients fetch via `entry.read({target})`. **Every notification envelope carries its `sessionId`** (and `runId` where the emitter has it) so a multi-session client — one connection, many sessions — can route it ({§notifications-envelope-carries-sessionid}); the broadcast stays session-scoped too.
@@ -1190,7 +1190,7 @@ The wire projection (`PacketWire.renderSlot`) groups sections by slot into the s
1190
1190
 
1191
1191
  **Prompt as a first-class entry.** Each loop's prompt is written on loop start as a plurnk-origin `EDIT` against `plurnk:///prompt/<loop_id>` (indexable, body channel, text/markdown). At render time the current loop's prompt body materializes into the `prompt` section; the entry itself stays READ/FOLD-able like any other. The foisted `EDIT`'s **log row is folded by default** (`expanded=0`): the prompt body already lives in the `prompt` section, so the log keeps the action for forensics while collapsing the duplicate body, re-OPENable like any fold (§open-fold). {§prompt-fold}
1192
1192
 
1193
- **The entry catalog.** `plurnk:///manifest.json` is a real session entry the model READs to discover what's available — rewritten every turn as a live view of the full entry set. Built in the schemes layer (`_entry-manifest`) and materialized like any entry (the engine only orchestrates the per-turn write — the same pattern as git membership), so it's READable and queryable. Body is `application/json`: a flat, **complete, unranked** array — one item per entry across all schemes, every entry listed in no relevance order, each `{ path, tags?, channels: { <name>: { mimetype, tokens, lines } } }`. `tags` is present only when the entry carries `entry_tags` — its own categorization, surfaced so the model sees it in the directory and can `FIND` by tag without a separate read. The model ranks and filters the catalog itself by querying it (task-aware); the catalog never ranks for it — the instant it did, it would be an index again. `tokens` is the provider's write-time count (budget depth), `lines` the content extent from `Mimetypes.process().totalLines`. The engine counts neither. It does not list itself. {§packet-manifest-catalog}
1193
+ **The entry catalog.** `plurnk:///manifest.json` is a real session entry the model READs to discover what's available — rewritten every turn as a live view of the full entry set. Built in the schemes layer (`_entry-manifest`) and materialized like any entry (the engine only orchestrates the per-turn write — the same pattern as git membership), so it's READable and queryable. Body is `application/json`: a flat, **complete, unranked** array — one item per entry across all schemes, every entry listed in no relevance order, each `{ path, tags?, channels: { <uri>: { mimetype, tokens, lines } } }` — every channel keyed by the URI the model READs (the default channel by the bare path, a non-default by `path#channel`), so it reaches a channel without guessing. `tags` is present only when the entry carries `entry_tags` — its own categorization, surfaced so the model sees it in the directory and can `FIND` by tag without a separate read. The model ranks and filters the catalog itself by querying it (task-aware); the catalog never ranks for it — the instant it did, it would be an index again. `tokens` is the provider's write-time count (budget depth), `lines` the content extent from `Mimetypes.process().totalLines`. The engine counts neither. It does not list itself. {§packet-manifest-catalog}
1194
1194
 
1195
1195
  ### §telemetry user.telemetry — model-facing runtime telemetry
1196
1196
 
@@ -1230,11 +1230,15 @@ Strike accounting, cycle detection, sudden-death thresholds, and no-ops bookkeep
1230
1230
 
1231
1231
  A `# Plurnk System Tools` section renders **above** `# Plurnk System Requirements` — a hook-populated list of the capabilities enabled this session, so the model sees what it can *do* before the rules it must follow. Each enabled capability contributes one line via `Engine.#collectTools`; the whole section is omitted when nothing is enabled. {§tools-capability-sheet}
1232
1232
 
1233
- **Contributors: the wired executor tags.** Each available executor tag injects a line describing its tag and functionality (the boot `ExecutorRegistry` probes availability per tag), retiring the model's blind `<<EXEC[sh]…`.
1233
+ **Contributors: the wired executor tags.** Each available executor tag *with an example* contributes ONE line its canonical usage — plus a `(docs: plurnk://docs/<tag>.md)` pointer when its package ships documentation, via the shared `teachingLine` (identical shape to the scheme directory, §schemes). A tag with no example contributes nothing; `PLURNK_DOCS_EXCLUDE` drops a named tag's line + doc. The boot `ExecutorRegistry` probes availability per tag, retiring the model's blind `<<EXEC[sh]…`.
1234
1234
 
1235
1235
  ### §schemes user.schemes — the scheme directory
1236
1236
 
1237
- A `# Plurnk System Schemes` section renders in the system slot **after the definition (plurnk.md — grammar + imperatives) and the tools sheet** — a terse directory of the scheme families available this session, so the model knows what URI schemes exist before it acts. Each scheme that ships a `manifest.example` contributes ONE line — its canonical usage — plus a `(docs: plurnk://docs/<scheme>.md)` pointer when it ships `manifest.documentation`. The verbose per-scheme semantics live in that pull doc (materialized like any entry, READ on demand), not the hot path — terse pushes, depth pulls, mirroring the tools sheet (§tools). A scheme with no example (provisional) is omitted. {§schemes-directory}
1237
+ A `# Plurnk System Schemes` section renders in the system slot **after the definition (plurnk.md — grammar + imperatives) and the tools sheet** — a terse directory of the scheme families available this session, so the model knows what URI schemes exist before it acts. Each scheme that ships a `manifest.example` contributes ONE line — its canonical usage (no scheme prefix; the example self-documents) — plus a `(docs: plurnk://docs/<scheme>.md)` pointer when a doc exists. The in-tree core schemes author their depth in `docs/<name>.md` (loaded at boot, shipped with the package); daughter schemes ship `manifest.documentation`. The verbose semantics live in that pull doc (materialized like any entry, READ on demand), not the hot path — terse pushes, depth pulls, via the same `teachingLine` as the tools sheet (§tools). A scheme with no example (provisional) is omitted; `PLURNK_DOCS_EXCLUDE` drops a named scheme's line + doc. {§schemes-directory}
1238
+
1239
+ ### §inject system.inject — the operator injection
1240
+
1241
+ 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}
1238
1242
 
1239
1243
  **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.
1240
1244
 
@@ -1 +1 @@
1
- {"version":3,"file":"Engine.d.ts","sourceRoot":"","sources":["../../src/core/Engine.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Engine.d.ts","sourceRoot":"","sources":["../../src/core/Engine.ts"],"names":[],"mappings":"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"}
@@ -8,6 +8,8 @@ import { foldAuthorityIntoPath, renderAddress } from "./plurnk-uri.js";
8
8
  import GitState from "./git-state.js";
9
9
  import Fork from "./fork.js";
10
10
  import RunCap from "./run-cap.js";
11
+ import { teachingLine, docsExcludeSet } from "./teaching.js";
12
+ import { readPacketInject } from "./packet-inject.js";
11
13
  import SessionSettings from "./session-settings.js";
12
14
  import { decodePathParens } from "./path-decode.js";
13
15
  import { DEFAULT_LOOP_FLAGS } from "./scheme-types.js";
@@ -366,13 +368,12 @@ class Engine {
366
368
  }
367
369
  this.#loopAborts.set(loopId, loopAbort);
368
370
  // Cleanup splits by termination kind:
369
- // - "graceful" (loop emitted SEND[2xx]): in-flight streaming-scheme
370
- // spawns are ALLOWED to outlive the loop. They complete naturally,
371
- // write final channel state, and wake-on-completion (E.4) opens a
372
- // fresh loop in the same run if the model needs to react.
373
- // - "forceful" (max_turns, strike_threshold, external cancel,
374
- // non-2xx loop status): fire the loop-level abort so spawns
375
- // tear down immediately.
371
+ // - "graceful" (SEND[202] Accepted): in-flight streaming-scheme spawns
372
+ // are ALLOWED to outlive the loop they complete naturally, write final
373
+ // channel state, and wake-on-completion (E.4) opens a fresh loop. 202 is
374
+ // the only terminal that means "keep my async work."
375
+ // - "forceful" (SEND[200] done, max_turns, strike, cancel, budget, 4xx/5xx):
376
+ // fire the loop-level abort so leftover spawns tear down. "Done" reaps.
376
377
  const cleanup = (kind, reason) => {
377
378
  if (kind === "forceful" && !loopAbort.signal.aborted) {
378
379
  loopAbort.abort(reason ?? "loop_forceful_termination");
@@ -387,10 +388,10 @@ class Engine {
387
388
  if (row === undefined)
388
389
  throw new Error(`Engine.runLoop: loop ${loopId} not found`);
389
390
  if (row.status !== 102) {
390
- // Status 2xx = graceful (model said done); 4xx/5xx = forceful
391
- // (external cancel or upstream failure). The threshold splits
392
- // at 400 to match HTTP success/error semantics.
393
- cleanup(row.status < 400 ? "graceful" : "forceful", `loop_terminal_${row.status}`);
391
+ // Only 202 (Accepted) lets spawns outlive it IS the async wake
392
+ // contract (E.4). Every other terminal, 200 included, reaps: "done"
393
+ // must not leak running execs. Trust the code's declared intent.
394
+ cleanup(row.status === 202 ? "graceful" : "forceful", `loop_terminal_${row.status}`);
394
395
  return { turnIds, finalStatus: row.status, hitMaxTurns: false, reason: "external" };
395
396
  }
396
397
  if (maxTurns >= 0 && turnIds.length >= maxTurns) {
@@ -578,6 +579,7 @@ class Engine {
578
579
  wakeRunNotify: this.#wakeRunNotify,
579
580
  tokenize: this.#tokenize,
580
581
  mimetypes: this.#mimetypes,
582
+ defaultChannelFor: (s) => this.#schemes.defaultChannelFor(s),
581
583
  pushTelemetry: (event) => this.#pushTelemetry(sessionId, loopId, event),
582
584
  };
583
585
  // SPEC §membership D4/D5 — git-ls-files workspace membership, resolved at
@@ -656,14 +658,14 @@ class Engine {
656
658
  // queries log_entries scoped to the run — the prompt entry just
657
659
  // written (if turn 1) is part of that query result.
658
660
  let requestPacket = await this.#buildRequestPacket({
659
- initialMessages: messages, requirements, runId, loopId,
661
+ initialMessages: messages, requirements, sessionId, runId, loopId,
660
662
  currentTurnSeq: seq, provider, gitStatus,
661
663
  });
662
664
  // SPEC §grinder — budget grinder, pre-LLM: reclaim window on actual overflow.
663
665
  const enforced = await this.#enforceBudget({
664
666
  packet: requestPacket, provider, runId, loopId, turnId, sessionId, turnNumber,
665
667
  rebuild: (telemetryErrors) => this.#buildRequestPacket({
666
- initialMessages: messages, requirements, runId, loopId,
668
+ initialMessages: messages, requirements, sessionId, runId, loopId,
667
669
  currentTurnSeq: seq, provider, telemetryErrors, gitStatus,
668
670
  }),
669
671
  });
@@ -694,14 +696,16 @@ class Engine {
694
696
  response = await provider.generate({ messages: modelMessages, runId: String(runId), signal, grammar: await this.#grammarConstraint(), maxTokens, attributions: attributions.length > 0 ? attributions : undefined }); // §provider-surface-generate §provider-guarantees-single-call §provider-guarantees-signal-wired §attribution-plurnk-namespace-reserved
695
697
  }
696
698
  catch (err) {
697
- // #256 grammar_unenforced is the one provider error the MODEL can recover from:
698
- // the backend didn't constrain the GBNF, so this turn's output was rejected, but a
699
- // conforming emission next turn is accepted. Surface it as telemetry (the model's
700
- // next packet shows it) and fall through as an empty no-op turn, so the strike rail
701
- // gives it maxStrikes chances before terminatingunlike infra errors (rate_limit,
702
- // network_failure, unauthorized), which propagate and end the loop.
703
- if (err instanceof ProviderError && err.kind === "grammar_unenforced") {
704
- this.#pushTelemetry(sessionId, loopId, { source: "provider", kind: "grammar_unenforced", message: err.message });
699
+ // Every provider error surfaces as telemetry (the client/model sees the cause). #256:
700
+ // grammar_unenforced is the one the MODEL can recover from the backend didn't
701
+ // constrain the GBNF, so this turn was rejected but a conforming emission next turn is
702
+ // accepted: fall through as an empty no-op turn so the strike rail retries. Every other
703
+ // kind (rate_limit, network_failure, unauthorized, …) is terminal telemetry'd, then
704
+ // propagated to end the loop (rather than only the opaque loop.run rejection).
705
+ if (err instanceof ProviderError) {
706
+ this.#pushTelemetry(sessionId, loopId, { source: "provider", kind: err.kind, message: err.message });
707
+ if (err.kind !== "grammar_unenforced")
708
+ throw err;
705
709
  response = {
706
710
  assistant: { content: "", reasoning: null, usage: { prompt: requestPacket.tokens, completion: 0, reasoning: 0, cached: 0, total: requestPacket.tokens }, finishReason: null, model: provider.model },
707
711
  assistantRaw: null,
@@ -881,7 +885,7 @@ class Engine {
881
885
  // and §user) BEFORE the provider call. The same packet object is then
882
886
  // completed with assistant + assistantRaw after the model responds, so
883
887
  // the stored packet and the wire payload share one source of truth.
884
- async #buildRequestPacket({ initialMessages, requirements, runId, loopId, currentTurnSeq, provider, gitStatus, telemetryErrors: presetTelemetry, }) {
888
+ async #buildRequestPacket({ initialMessages, requirements, sessionId, runId, loopId, currentTurnSeq, provider, gitStatus, telemetryErrors: presetTelemetry, }) {
885
889
  const byRole = (role) => initialMessages.filter((m) => m.role === role).map((m) => m.content).join("\n\n");
886
890
  // plurnk.md (grammar/dialects) ONLY — the definition is the hot-path grammar.
887
891
  // The scheme catalogue is its own `schemes` section below tools (§schemes-directory),
@@ -921,6 +925,9 @@ class Engine {
921
925
  // omitted, section lines still shown). §tokenomics-render-weight-budget
922
926
  const ceiling = _a.computeCeiling(provider.contextSize, this.#budgetCeiling);
923
927
  const budgetReadout = this.#renderBudget(PacketWire.measureLogBudget(log, countTokens), ceiling);
928
+ // Per-scheme tally (§packet) so the model sees which schemes hold content without
929
+ // probing e.g. FIND(known://**) every turn. "" when empty → the section is omitted.
930
+ const catalogSummary = await this.#db.engine_scheme_catalog_summary.all({ session_id: sessionId });
924
931
  // The default packet: an ordered list of sections, each addressable state
925
932
  // (§packet-construction). `slot` is the prompt-cache boundary; the STATIC
926
933
  // sections (definition, tools) lead the system slot so they form the cached
@@ -928,15 +935,18 @@ class Engine {
928
935
  // last (the contract closest to the assistant turn); budget/errors/git are
929
936
  // peer sections (unbundled). The budget section carries its {{tokensFree}}
930
937
  // placeholders here; they resolve below once the assembled total is known.
938
+ const inject = await readPacketInject(); // #240 — operator section, per-turn, fail-hard on a broken path
931
939
  const defaults = [
932
940
  { name: "definition", slot: "system", header: null, content: system_definition, tokens: 0 },
933
941
  { name: "tools", slot: "system", header: "Plurnk System Tools", content: tools.join("\n"), tokens: 0 },
934
942
  { name: "schemes", slot: "system", header: "Plurnk System Schemes", content: this.#schemes.teach(), tokens: 0 },
943
+ ...(inject !== null ? [{ name: "inject", slot: "system", header: "Plurnk Operator Notes", content: inject, tokens: 0 }] : []),
935
944
  { name: "log", slot: "system", header: "Plurnk System Log", content: PacketWire.renderLog(log), tokens: 0 },
936
945
  { name: "prompt", slot: "user", header: "Plurnk System User Prompt", content: prompt, tokens: 0 },
937
946
  { name: "budget", slot: "user", header: "Plurnk System Budget", content: budgetReadout, tokens: 0 },
938
947
  { name: "errors", slot: "user", header: "Plurnk System Errors", content: PacketWire.renderErrors(telemetryErrors), tokens: 0 },
939
948
  { name: "git", slot: "user", header: "Plurnk System Git Status", content: PacketWire.renderGit(gitStatus), tokens: 0 },
949
+ { name: "catalog", slot: "user", header: "Plurnk System Catalog", content: PacketWire.renderCatalog(catalogSummary), tokens: 0 },
940
950
  { name: "requirements", slot: "user", header: "Plurnk System Requirements", content: requirementsText, tokens: 0 },
941
951
  ];
942
952
  // Plugin packet control (§packet-construction): trusted schemes rewrite the
@@ -1006,14 +1016,16 @@ class Engine {
1006
1016
  // bullets + bare op forms match the packet's list/op rendering (no `- `,
1007
1017
  // no backticks — see packet-wire.ts).
1008
1018
  if (this.#executors !== undefined) {
1019
+ const excluded = docsExcludeSet();
1009
1020
  for (const tag of this.#executors.availableRuntimes()) {
1021
+ if (excluded.has(tag))
1022
+ continue; // #240 — PLURNK_DOCS_EXCLUDE drops the oneliner + the doc
1010
1023
  const entry = this.#executors.entry(tag);
1024
+ // #240 — identical treatment with the scheme directory: the example IS the oneliner,
1025
+ // the fuller doc (materialized at plurnk://docs/<tag>.md) rides an inline link whose
1026
+ // token cost lives on that manifest entry. No example → no line (like a provisional scheme).
1011
1027
  if (entry?.example)
1012
- tools.push(`* ${entry.example}`);
1013
- // #note12 — link the executor's fuller doc (materialized at plurnk:///docs/<tag>.md);
1014
- // its token cost rides that manifest entry, so no inline recount here.
1015
- if (entry?.documentation)
1016
- tools.push(`* docs for ${tag}: plurnk://docs/${tag}.md`);
1028
+ tools.push(teachingLine(entry.example, tag, Boolean(entry.documentation)));
1017
1029
  }
1018
1030
  }
1019
1031
  return tools;
@@ -1022,9 +1034,12 @@ class Engine {
1022
1034
  // materialized at plurnk:///docs/<name>.md by loop_run (like operator docs) so the
1023
1035
  // catalogue's doc-links READ and the manifest carries each doc's token cost.
1024
1036
  docEntries() {
1025
- const out = this.#schemes.docs();
1037
+ const out = this.#schemes.docs(); // scheme docs already drop PLURNK_DOCS_EXCLUDE names
1026
1038
  if (this.#executors !== undefined) {
1039
+ const excluded = docsExcludeSet();
1027
1040
  for (const tag of this.#executors.availableRuntimes()) {
1041
+ if (excluded.has(tag))
1042
+ continue; // #240 — exec docs honor the same exclude
1028
1043
  const doc = this.#executors.entry(tag)?.documentation;
1029
1044
  if (doc !== undefined && doc.length > 0)
1030
1045
  out.push({ name: tag, content: doc });
@@ -1752,11 +1767,12 @@ class Engine {
1752
1767
  return { status: 400, error: "KILL target must be a URL path with a scheme" };
1753
1768
  if (schemeName === "log")
1754
1769
  return { status: 405, error: "log:/// is append-only; KILL must bounce" };
1755
- if (schemeName === "exec") {
1756
- const execHandler = this.#schemes.get("exec");
1757
- if (execHandler === undefined || typeof execHandler.kill !== "function")
1758
- return { status: 501 };
1759
- return await execHandler.kill(pathnameFromPath(path), ctx);
1770
+ // Process-KILL: any scheme whose handler exposes kill() aborts a live stream — the
1771
+ // exec handler, registered as "exec" + under every runtime tag (sh/node), so a tag-
1772
+ // addressed stream (sh:///l/t/s) routes here, not to deleteEntry. §exec
1773
+ const killable = this.#schemes.get(schemeName);
1774
+ if (killable !== undefined && typeof killable.kill === "function") {
1775
+ return await killable.kill(pathnameFromPath(path), statement.signal, ctx);
1760
1776
  }
1761
1777
  if (schemeName === "run") {
1762
1778
  // terminate — abort any run by address; whoever holds it may end it.
@@ -1873,8 +1889,9 @@ class Engine {
1873
1889
  const status = statement.signal;
1874
1890
  if (status === null)
1875
1891
  return { status: 400 };
1876
- if (status === 200 || status === 499) {
1877
- // the loop's terminal message its deliverable rides the termination delta.
1892
+ if (status === 200 || status === 202 || status === 499) {
1893
+ // The broadcast terminals (200 done, 202 parked-async, 499 cancelled) advance
1894
+ // the loop; each carries its body as the loop's terminal message — the deliverable.
1878
1895
  const body = statement.body;
1879
1896
  const message = body === null ? null : typeof body === "string" ? body : body.raw;
1880
1897
  await this.#db.engine_loop_set_status.run({ status, loop_id: loopId, message });
@@ -1934,15 +1951,13 @@ class Engine {
1934
1951
  let attrsObj = (result.attrs !== undefined && result.attrs !== null)
1935
1952
  ? { ...result.attrs }
1936
1953
  : {};
1937
- // EXEC pathname is executor-domain + coordinate: the stream entry
1938
- // lives at exec:///<runtime>/<loop_seq>/<turn_seq>/<sequence> (e.g.
1939
- // exec:///sh/1/1/2). The runtime leads domain-aware, the executor
1940
- // as authority and the coordinate that follows is already unique
1941
- // per statement, so no slug is injected. The log row's target points
1942
- // at this same address; its log:/// coordinate shares the trailing
1943
- // <loop>/<turn>/<seq>, so the model correlates op to stream output.
1944
- // Runtime comes from statement.signal (EXEC's runtime slot) so it's
1945
- // resolvable for failed execs too; empty/absent = the default shell.
1954
+ // EXEC stream entry addresses by RUNTIME TAG as authority (§exec): it lives at
1955
+ // <runtime>:///<loop_seq>/<turn_seq>/<sequence> (e.g. sh:///1/1/2) — the runtime tag
1956
+ // is the scheme, the coordinate already unique per statement. The log row's target
1957
+ // points at this same address; its log:/// coordinate shares the trailing
1958
+ // <loop>/<turn>/<seq>, so the model correlates op to stream output. Runtime comes
1959
+ // from statement.signal (EXEC's runtime slot) so it's resolvable for failed execs
1960
+ // too; empty/absent = the default shell.
1946
1961
  if (statement.op === "EXEC") {
1947
1962
  const seqs = await this.#db.engine_loop_turn_seqs.get({
1948
1963
  loop_id: loopId, turn_id: turnId,
@@ -1950,8 +1965,8 @@ class Engine {
1950
1965
  if (seqs === undefined)
1951
1966
  throw new Error(`Engine.#writeLog: loop_turn_seqs returned no row for loop=${loopId} turn=${turnId}`);
1952
1967
  const runtime = (typeof statement.signal === "string" && statement.signal.length > 0) ? statement.signal : "sh";
1953
- const coordPathname = `/${runtime}/${seqs.loop_seq}/${seqs.turn_seq}/${sequence}`;
1954
- target.scheme = "exec";
1968
+ const coordPathname = `/${seqs.loop_seq}/${seqs.turn_seq}/${sequence}`;
1969
+ target.scheme = runtime;
1955
1970
  target.pathname = coordPathname;
1956
1971
  attrsObj.pathname = coordPathname;
1957
1972
  // Mutate the in-memory result.attrs too: the dispatch path