@plurnk/plurnk-service 0.45.0 → 0.46.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 (46) hide show
  1. package/.env.example +14 -1
  2. package/README.md +1 -1
  3. package/SPEC.md +5 -5
  4. package/dist/core/Engine.d.ts.map +1 -1
  5. package/dist/core/Engine.js +46 -40
  6. package/dist/core/Engine.js.map +1 -1
  7. package/dist/core/Engine.sql +16 -0
  8. package/dist/core/ExecutorRegistry.d.ts.map +1 -1
  9. package/dist/core/ExecutorRegistry.js +7 -2
  10. package/dist/core/ExecutorRegistry.js.map +1 -1
  11. package/dist/core/SchemeRegistry.d.ts +2 -0
  12. package/dist/core/SchemeRegistry.d.ts.map +1 -1
  13. package/dist/core/SchemeRegistry.js +31 -0
  14. package/dist/core/SchemeRegistry.js.map +1 -1
  15. package/dist/core/packet-wire.d.ts +1 -0
  16. package/dist/core/packet-wire.d.ts.map +1 -1
  17. package/dist/core/packet-wire.js +11 -0
  18. package/dist/core/packet-wire.js.map +1 -1
  19. package/dist/core/scheme-types.d.ts +1 -0
  20. package/dist/core/scheme-types.d.ts.map +1 -1
  21. package/dist/schemes/Exec.d.ts +2 -2
  22. package/dist/schemes/Exec.d.ts.map +1 -1
  23. package/dist/schemes/Exec.js +30 -25
  24. package/dist/schemes/Exec.js.map +1 -1
  25. package/dist/schemes/_entry-manifest.d.ts.map +1 -1
  26. package/dist/schemes/_entry-manifest.js +7 -2
  27. package/dist/schemes/_entry-manifest.js.map +1 -1
  28. package/dist/schemes/_entry-semantic.d.ts.map +1 -1
  29. package/dist/schemes/_entry-semantic.js +14 -7
  30. package/dist/schemes/_entry-semantic.js.map +1 -1
  31. package/dist/schemes/_entry-semantic.sql +16 -0
  32. package/dist/schemes/exec-abort.d.ts +11 -0
  33. package/dist/schemes/exec-abort.d.ts.map +1 -0
  34. package/dist/schemes/exec-abort.js +23 -0
  35. package/dist/schemes/exec-abort.js.map +1 -0
  36. package/dist/server/Daemon.d.ts.map +1 -1
  37. package/dist/server/Daemon.js +62 -42
  38. package/dist/server/Daemon.js.map +1 -1
  39. package/dist/server/drain.sql +10 -0
  40. package/dist/server/methods/loop_run.d.ts.map +1 -1
  41. package/dist/server/methods/loop_run.js +31 -11
  42. package/dist/server/methods/loop_run.js.map +1 -1
  43. package/dist/service.js +1 -1
  44. package/dist/service.js.map +1 -1
  45. package/migrations/0000-00-00.01_schema.sql +2 -2
  46. package/package.json +9 -9
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 ---
@@ -141,6 +150,10 @@ PLURNK_VERSION_POLL_TTL=3600000
141
150
  # @plurnk/plurnk-grammar; an absolute/relative path is your own. =0 (or empty) disables.
142
151
  # PLURNK_PROVIDERS_GBNF=plurnk.gbnf
143
152
 
153
+ # GBNF debug, honored natively by @plurnk/plurnk-providers (>=0.13.0): validate the grammar locally
154
+ # (fail-hard if malformed) and run UNCONSTRAINED, never sending it to the model. Dev aid; off by default.
155
+ PLURNK_GBNF_DEBUG=0
156
+
144
157
  # Provider retry attempts (providers 0.7+): how many times generate() retries a
145
158
  # TRANSIENT failure (429 rate-limit, 5xx/network) with exponential backoff (base 2s
146
159
  # is a provider constant — the COUNT is the operator knob). REQUIRED, fail-hard if
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
  ---
@@ -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
 
@@ -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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"}
@@ -366,13 +366,12 @@ class Engine {
366
366
  }
367
367
  this.#loopAborts.set(loopId, loopAbort);
368
368
  // 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.
369
+ // - "graceful" (SEND[202] Accepted): in-flight streaming-scheme spawns
370
+ // are ALLOWED to outlive the loop they complete naturally, write final
371
+ // channel state, and wake-on-completion (E.4) opens a fresh loop. 202 is
372
+ // the only terminal that means "keep my async work."
373
+ // - "forceful" (SEND[200] done, max_turns, strike, cancel, budget, 4xx/5xx):
374
+ // fire the loop-level abort so leftover spawns tear down. "Done" reaps.
376
375
  const cleanup = (kind, reason) => {
377
376
  if (kind === "forceful" && !loopAbort.signal.aborted) {
378
377
  loopAbort.abort(reason ?? "loop_forceful_termination");
@@ -387,10 +386,10 @@ class Engine {
387
386
  if (row === undefined)
388
387
  throw new Error(`Engine.runLoop: loop ${loopId} not found`);
389
388
  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}`);
389
+ // Only 202 (Accepted) lets spawns outlive it IS the async wake
390
+ // contract (E.4). Every other terminal, 200 included, reaps: "done"
391
+ // must not leak running execs. Trust the code's declared intent.
392
+ cleanup(row.status === 202 ? "graceful" : "forceful", `loop_terminal_${row.status}`);
394
393
  return { turnIds, finalStatus: row.status, hitMaxTurns: false, reason: "external" };
395
394
  }
396
395
  if (maxTurns >= 0 && turnIds.length >= maxTurns) {
@@ -578,6 +577,7 @@ class Engine {
578
577
  wakeRunNotify: this.#wakeRunNotify,
579
578
  tokenize: this.#tokenize,
580
579
  mimetypes: this.#mimetypes,
580
+ defaultChannelFor: (s) => this.#schemes.defaultChannelFor(s),
581
581
  pushTelemetry: (event) => this.#pushTelemetry(sessionId, loopId, event),
582
582
  };
583
583
  // SPEC §membership D4/D5 — git-ls-files workspace membership, resolved at
@@ -656,14 +656,14 @@ class Engine {
656
656
  // queries log_entries scoped to the run — the prompt entry just
657
657
  // written (if turn 1) is part of that query result.
658
658
  let requestPacket = await this.#buildRequestPacket({
659
- initialMessages: messages, requirements, runId, loopId,
659
+ initialMessages: messages, requirements, sessionId, runId, loopId,
660
660
  currentTurnSeq: seq, provider, gitStatus,
661
661
  });
662
662
  // SPEC §grinder — budget grinder, pre-LLM: reclaim window on actual overflow.
663
663
  const enforced = await this.#enforceBudget({
664
664
  packet: requestPacket, provider, runId, loopId, turnId, sessionId, turnNumber,
665
665
  rebuild: (telemetryErrors) => this.#buildRequestPacket({
666
- initialMessages: messages, requirements, runId, loopId,
666
+ initialMessages: messages, requirements, sessionId, runId, loopId,
667
667
  currentTurnSeq: seq, provider, telemetryErrors, gitStatus,
668
668
  }),
669
669
  });
@@ -694,14 +694,16 @@ class Engine {
694
694
  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
695
  }
696
696
  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 });
697
+ // Every provider error surfaces as telemetry (the client/model sees the cause). #256:
698
+ // grammar_unenforced is the one the MODEL can recover from the backend didn't
699
+ // constrain the GBNF, so this turn was rejected but a conforming emission next turn is
700
+ // accepted: fall through as an empty no-op turn so the strike rail retries. Every other
701
+ // kind (rate_limit, network_failure, unauthorized, …) is terminal telemetry'd, then
702
+ // propagated to end the loop (rather than only the opaque loop.run rejection).
703
+ if (err instanceof ProviderError) {
704
+ this.#pushTelemetry(sessionId, loopId, { source: "provider", kind: err.kind, message: err.message });
705
+ if (err.kind !== "grammar_unenforced")
706
+ throw err;
705
707
  response = {
706
708
  assistant: { content: "", reasoning: null, usage: { prompt: requestPacket.tokens, completion: 0, reasoning: 0, cached: 0, total: requestPacket.tokens }, finishReason: null, model: provider.model },
707
709
  assistantRaw: null,
@@ -881,7 +883,7 @@ class Engine {
881
883
  // and §user) BEFORE the provider call. The same packet object is then
882
884
  // completed with assistant + assistantRaw after the model responds, so
883
885
  // the stored packet and the wire payload share one source of truth.
884
- async #buildRequestPacket({ initialMessages, requirements, runId, loopId, currentTurnSeq, provider, gitStatus, telemetryErrors: presetTelemetry, }) {
886
+ async #buildRequestPacket({ initialMessages, requirements, sessionId, runId, loopId, currentTurnSeq, provider, gitStatus, telemetryErrors: presetTelemetry, }) {
885
887
  const byRole = (role) => initialMessages.filter((m) => m.role === role).map((m) => m.content).join("\n\n");
886
888
  // plurnk.md (grammar/dialects) ONLY — the definition is the hot-path grammar.
887
889
  // The scheme catalogue is its own `schemes` section below tools (§schemes-directory),
@@ -921,6 +923,9 @@ class Engine {
921
923
  // omitted, section lines still shown). §tokenomics-render-weight-budget
922
924
  const ceiling = _a.computeCeiling(provider.contextSize, this.#budgetCeiling);
923
925
  const budgetReadout = this.#renderBudget(PacketWire.measureLogBudget(log, countTokens), ceiling);
926
+ // Per-scheme tally (§packet) so the model sees which schemes hold content without
927
+ // probing e.g. FIND(known://**) every turn. "" when empty → the section is omitted.
928
+ const catalogSummary = await this.#db.engine_scheme_catalog_summary.all({ session_id: sessionId });
924
929
  // The default packet: an ordered list of sections, each addressable state
925
930
  // (§packet-construction). `slot` is the prompt-cache boundary; the STATIC
926
931
  // sections (definition, tools) lead the system slot so they form the cached
@@ -937,6 +942,7 @@ class Engine {
937
942
  { name: "budget", slot: "user", header: "Plurnk System Budget", content: budgetReadout, tokens: 0 },
938
943
  { name: "errors", slot: "user", header: "Plurnk System Errors", content: PacketWire.renderErrors(telemetryErrors), tokens: 0 },
939
944
  { name: "git", slot: "user", header: "Plurnk System Git Status", content: PacketWire.renderGit(gitStatus), tokens: 0 },
945
+ { name: "catalog", slot: "user", header: "Plurnk System Catalog", content: PacketWire.renderCatalog(catalogSummary), tokens: 0 },
940
946
  { name: "requirements", slot: "user", header: "Plurnk System Requirements", content: requirementsText, tokens: 0 },
941
947
  ];
942
948
  // Plugin packet control (§packet-construction): trusted schemes rewrite the
@@ -1752,11 +1758,12 @@ class Engine {
1752
1758
  return { status: 400, error: "KILL target must be a URL path with a scheme" };
1753
1759
  if (schemeName === "log")
1754
1760
  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);
1761
+ // Process-KILL: any scheme whose handler exposes kill() aborts a live stream — the
1762
+ // exec handler, registered as "exec" + under every runtime tag (sh/node), so a tag-
1763
+ // addressed stream (sh:///l/t/s) routes here, not to deleteEntry. §exec
1764
+ const killable = this.#schemes.get(schemeName);
1765
+ if (killable !== undefined && typeof killable.kill === "function") {
1766
+ return await killable.kill(pathnameFromPath(path), statement.signal, ctx);
1760
1767
  }
1761
1768
  if (schemeName === "run") {
1762
1769
  // terminate — abort any run by address; whoever holds it may end it.
@@ -1873,8 +1880,9 @@ class Engine {
1873
1880
  const status = statement.signal;
1874
1881
  if (status === null)
1875
1882
  return { status: 400 };
1876
- if (status === 200 || status === 499) {
1877
- // the loop's terminal message its deliverable rides the termination delta.
1883
+ if (status === 200 || status === 202 || status === 499) {
1884
+ // The broadcast terminals (200 done, 202 parked-async, 499 cancelled) advance
1885
+ // the loop; each carries its body as the loop's terminal message — the deliverable.
1878
1886
  const body = statement.body;
1879
1887
  const message = body === null ? null : typeof body === "string" ? body : body.raw;
1880
1888
  await this.#db.engine_loop_set_status.run({ status, loop_id: loopId, message });
@@ -1934,15 +1942,13 @@ class Engine {
1934
1942
  let attrsObj = (result.attrs !== undefined && result.attrs !== null)
1935
1943
  ? { ...result.attrs }
1936
1944
  : {};
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.
1945
+ // EXEC stream entry addresses by RUNTIME TAG as authority (§exec): it lives at
1946
+ // <runtime>:///<loop_seq>/<turn_seq>/<sequence> (e.g. sh:///1/1/2) — the runtime tag
1947
+ // is the scheme, the coordinate already unique per statement. The log row's target
1948
+ // points at this same address; its log:/// coordinate shares the trailing
1949
+ // <loop>/<turn>/<seq>, so the model correlates op to stream output. Runtime comes
1950
+ // from statement.signal (EXEC's runtime slot) so it's resolvable for failed execs
1951
+ // too; empty/absent = the default shell.
1946
1952
  if (statement.op === "EXEC") {
1947
1953
  const seqs = await this.#db.engine_loop_turn_seqs.get({
1948
1954
  loop_id: loopId, turn_id: turnId,
@@ -1950,8 +1956,8 @@ class Engine {
1950
1956
  if (seqs === undefined)
1951
1957
  throw new Error(`Engine.#writeLog: loop_turn_seqs returned no row for loop=${loopId} turn=${turnId}`);
1952
1958
  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";
1959
+ const coordPathname = `/${seqs.loop_seq}/${seqs.turn_seq}/${sequence}`;
1960
+ target.scheme = runtime;
1955
1961
  target.pathname = coordPathname;
1956
1962
  attrsObj.pathname = coordPathname;
1957
1963
  // Mutate the in-memory result.attrs too: the dispatch path