@plurnk/plurnk-service 0.52.0 → 0.54.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 (39) hide show
  1. package/SPEC.md +34 -10
  2. package/dist/core/Engine.d.ts +1 -0
  3. package/dist/core/Engine.d.ts.map +1 -1
  4. package/dist/core/Engine.js +120 -37
  5. package/dist/core/Engine.js.map +1 -1
  6. package/dist/core/Engine.sql +39 -7
  7. package/dist/core/SchemeRegistry.d.ts.map +1 -1
  8. package/dist/core/SchemeRegistry.js +1 -2
  9. package/dist/core/SchemeRegistry.js.map +1 -1
  10. package/dist/core/packet-wire.d.ts +1 -2
  11. package/dist/core/packet-wire.d.ts.map +1 -1
  12. package/dist/core/packet-wire.js +111 -138
  13. package/dist/core/packet-wire.js.map +1 -1
  14. package/dist/core/teaching.d.ts +1 -1
  15. package/dist/core/teaching.d.ts.map +1 -1
  16. package/dist/core/teaching.js +6 -4
  17. package/dist/core/teaching.js.map +1 -1
  18. package/dist/schemes/Exec.d.ts.map +1 -1
  19. package/dist/schemes/Exec.js +11 -24
  20. package/dist/schemes/Exec.js.map +1 -1
  21. package/dist/schemes/Log.d.ts.map +1 -1
  22. package/dist/schemes/Log.js +3 -2
  23. package/dist/schemes/Log.js.map +1 -1
  24. package/dist/schemes/Run.js +1 -1
  25. package/dist/schemes/Run.js.map +1 -1
  26. package/dist/schemes/_entry-find.d.ts +1 -0
  27. package/dist/schemes/_entry-find.d.ts.map +1 -1
  28. package/dist/schemes/_entry-find.js +8 -2
  29. package/dist/schemes/_entry-find.js.map +1 -1
  30. package/dist/server/Daemon.d.ts.map +1 -1
  31. package/dist/server/Daemon.js +5 -8
  32. package/dist/server/Daemon.js.map +1 -1
  33. package/docs/log.md +1 -1
  34. package/package.json +16 -15
  35. package/requirements.md +3 -9
  36. package/dist/schemes/exec-receipt.d.ts +0 -4
  37. package/dist/schemes/exec-receipt.d.ts.map +0 -1
  38. package/dist/schemes/exec-receipt.js +0 -25
  39. package/dist/schemes/exec-receipt.js.map +0 -1
package/SPEC.md CHANGED
@@ -562,8 +562,26 @@ AST: `{ op: "FIND", target (scope), body: MatcherBody | null (predicate), signal
562
562
 
563
563
  AST: `{ op: "SEND", target: ParsedPath | null, body: SendBody | null, signal: number | null }`.
564
564
 
565
- - **Broadcast** (path null): terminal status (200/499) updates `loop.status` and ends loop. Other codes return `{status}` with no state change.
566
- - **Directed** (path non-null): routes to `scheme.send` per §send-dispatch.
565
+ - **Broadcast** (path null): the loop's disposition verb. `signal` is the model's *claim* about the run's state see the terminal contract.
566
+ - **Directed** (path non-null): routes to `scheme.send` per §send-dispatch — stream control / cross-run irc, never a loop terminal.
567
+
568
+ **Terminal contract — the model's surface.** A broadcast SEND's status is a claim the engine **verifies against the run's actual state**, never a verdict it trusts. The model is trusted with exactly four codes:
569
+
570
+ | signal | claim | effect |
571
+ |---|---|---|
572
+ | **102** | continue | turn closes, another turn fires. Not terminal. |
573
+ | **200** | done | terminal — *only* when the run holds no live stream/spawn; otherwise the Premature-Terminate state below fires. Updates `loop.status`, ends the loop. |
574
+ | **202** | hibernate | terminal-but-resumable: the loop **sleeps** awaiting a wake edge — a stream-status transition or a directed prompt (§actor-boundary-passive-wake, §run-lifecycle-wake-liveness). NOT advertised to the model: it lives in `run.md` and reaches the model only via the engine's steering, never the hot-path packet. Distinct from the dispatch-internal proposal-202 (§proposal), which the model never emits. |
575
+ | **499** | give up | terminal — the model's **one** self-decided failure (a self-cancel; 499 = cancelled, §state-terms). The only failure it is trusted to declare for itself. |
576
+
577
+ The engine's failure terminals — **500** (strike threshold) and **508** (cycle), §engine-rails — are never the model's to pick; they are the engine ruling the loop failed. The surface is small on purpose: the model says done, waiting, or giving up, and is never asked to hold a correct opinion about *how* it failed or *whether* it can be woken — the engine decides those from state.
578
+
579
+ **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
+
581
+ - **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."*
583
+
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.
567
585
 
568
586
  ### §exec EXEC
569
587
 
@@ -573,7 +591,9 @@ Engine routes unconditionally to `exec` scheme (path slot is `cwd`, not a URI).
573
591
 
574
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}
575
593
 
576
- 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}
594
+ 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-bandlike 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
+
596
+ **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}
577
597
 
578
598
  `SEND[499](exec:///<runtime>/<loop>/<turn>/<seq>)` cancels in-flight subprocess via subscription registry's stored AbortController (§stream-control).
579
599
 
@@ -1185,7 +1205,7 @@ The CAS is the **hard backstop**, at the moment of writing, on every accept path
1185
1205
  type PacketSection = {
1186
1206
  name: string; // stable id: definition, tools, schemes, log, prompt, budget, errors, git, requirements — or a plugin's own
1187
1207
  slot: "system" | "user"; // the prompt-cache boundary; system-slot sections build the cache-stable system message
1188
- header: string | null; // "# Plurnk System X", or null (definition renders verbatim)
1208
+ header: string | null; // "## Plurnk System X", or null (definition renders verbatim)
1189
1209
  content: string; // rendered markdown — what the model saw
1190
1210
  tokens: number; // measured render-weight
1191
1211
  };
@@ -1217,7 +1237,7 @@ Telemetry the model MUST react to immediately. Errors render in their own `error
1217
1237
 
1218
1238
  - `budget` per §tokenomics: turn-weight and heaviest-entries tables with `tokenCeiling`/`tokenUsage`/`tokensFree`.
1219
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.
1220
- - Wire: one `* {canonical-JSON}` line per error under `# Plurnk System Errors`, push order. Buffer drains on read. {§telemetry-drain-on-read}
1240
+ - Wire: one `* {canonical-JSON}` line per error under `## Plurnk System Errors`, push order. Buffer drains on read. {§telemetry-drain-on-read}
1221
1241
  - **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.
1222
1242
  - **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.
1223
1243
 
@@ -1240,23 +1260,23 @@ Strike accounting, cycle detection, sudden-death thresholds, and no-ops bookkeep
1240
1260
 
1241
1261
  ### §tools user.tools — the capability sheet
1242
1262
 
1243
- The tools capability lines render **titleless**, directly under the `definition` (plurnk.md) section — the examples flow on from plurnk.md with no separate header — and **above** `# Plurnk System Requirements`, so the model sees what it can *do* before the rules it must follow. Each enabled capability contributes one line via `Engine.#collectTools`; the section is omitted when nothing is enabled. {§tools-capability-sheet}
1263
+ The tools capability lines render **titleless**, directly under the `definition` (plurnk.md) section — the examples flow on from plurnk.md with no separate header — and **above** `## Plurnk System Requirements`, so the model sees what it can *do* before the rules it must follow. Each enabled capability contributes one line via `Engine.#collectTools`; the section is omitted when nothing is enabled. {§tools-capability-sheet}
1244
1264
 
1245
- **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]…`.
1265
+ **Contributors: the wired executor tags.** Each available executor tag *with an example* contributes ONE line — its canonical usage — via the shared `teachingLine` (identical shape to the scheme directory, §schemes); its doc is materialized at `plurnk://docs/<tag>.md` and discovered via the turn-1 `FIND(plurnk://docs/**)` foist, not linked inline (#270). 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]…`.
1246
1266
 
1247
1267
  ### §schemes user.schemes — the scheme directory
1248
1268
 
1249
- 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}
1269
+ 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). The doc is NOT linked inline (#270) it is materialized at `plurnk://docs/<scheme>.md` and discovered via the turn-1 `FIND(plurnk://docs/**)` foist, keeping the raw packet free of doc links. 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}
1250
1270
 
1251
1271
  ### §inject system.inject — the operator injection
1252
1272
 
1253
- 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}
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}
1254
1274
 
1255
1275
  **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.
1256
1276
 
1257
1277
  ### §requirements The requirements section — static per-turn rules
1258
1278
 
1259
- Rendered at the END of the user packet under `# Plurnk System Requirements` {§requirements-requirements-render-last} — closest to the assistant turn so the contract the model has to honor is the most recent text it sees. The header is omitted entirely when the requirements string is empty. {§requirements-requirements-omitted-when-empty} Contains rules the grammar block doesn't cover (canonical example: "Conclude the loop with `<<SEND[200]:answer:SEND`"). The op syntax leads the section. PLAN is mandated unconditionally by plurnk.md §Imperatives (grammar 0.70 requires every turn to lead with `<<PLAN`), so the service injects no separate plan directive here.
1279
+ Rendered at the END of the user packet under `## Plurnk System Requirements` {§requirements-requirements-render-last} — closest to the assistant turn so the contract the model has to honor is the most recent text it sees. The header is omitted entirely when the requirements string is empty. {§requirements-requirements-omitted-when-empty} Contains rules the grammar block doesn't cover (canonical example: "Conclude the loop with `<<SEND[200]:answer:SEND`"). The op syntax leads the section. PLAN is mandated unconditionally by plurnk.md §Imperatives (grammar 0.70 requires every turn to lead with `<<PLAN`), so the service injects no separate plan directive here.
1260
1280
 
1261
1281
  **Sourcing:** caller supplies the string via `runLoop({ requirements })` / `runTurn({ requirements })`. Plurnk-service exposes `PATHS.defaultRequirements` (resolves `PLURNK_REQUIREMENTS` env → in-package `requirements.md`). No DB cascade — same string every turn.
1262
1282
 
@@ -1376,6 +1396,10 @@ Same rule applies across Known, Unknown, Skill, Plurnk, File. Effective mimetype
1376
1396
  - **Line-navigable** (text/markdown, text/plain, csv, source code, yaml, toml) → `N:\t` line-number prefix per line {§render-rule-line-navigable-prefix}
1377
1397
  - **Tree-navigable** (application/json, application/xml, text/html, +json/+xml suffixes) → verbatim body (no `N:\t` — outer line numbers would collide with structural navigation like jsonpath/xpath) {§render-rule-tree-navigable-verbatim}
1378
1398
 
1399
+ A log row renders its **result body** for the content-returning ops — `READ@200` (the content it pulled) and `FIND@200` (the catalog rows / matched entries it returned) — under the query's fence, mimetype-driven per the rules above; every other op re-emits its statement. FIND included: the model must see what a find *returned*, not just its echoed query, and the turn-0 foisted `FIND(scheme:///**)` reaches the packet through this branch — without it the catalog preview is invisible. {§render-rule-find-renders-result}
1400
+
1401
+ An `EDIT` log row renders its **resulting span** — the edited area as it looks now (`rx.span`), under the target's fence — not the input statement: the log reads "and here's X now," so the model sees its edit's effect. The meta line still carries op + target; the model's own EDITs and the system delta-EDITs (§env-delta) render identically; an emptied span → meta line only. With no span stored, the row falls back to re-emitting the statement (the heredoc the model wrote). {§edit-result-render}
1402
+
1379
1403
  The `N:\t` prefix is presentation/reference per plurnk.md ("not part of the source"); stripped before any matcher operation on the log entry.
1380
1404
 
1381
1405
  ### §markdown-primitive Mimetype primitive: text/markdown
@@ -115,6 +115,7 @@ export default class Engine {
115
115
  fingerprint: string;
116
116
  budgetStruck: boolean;
117
117
  budgetHardStop: boolean;
118
+ steerStruck: boolean;
118
119
  }>;
119
120
  docEntries(): Array<{
120
121
  name: string;
@@ -1 +1 @@
1
- {"version":3,"file":"Engine.d.ts","sourceRoot":"","sources":["../../src/core/Engine.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAA0F,MAAM,wBAAwB,CAAC;AAMtJ,OAAO,KAAK,cAAc,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,SAAS,EAAiB,MAAM,0BAA0B,CAAC;AACpE,OAAO,KAAK,EAAE,EAAE,EAAc,MAAM,SAAS,CAAC;AAa9C,OAAO,KAAK,EAAkB,UAAU,EAAuB,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACpG,OAAO,KAAK,gBAAgB,MAAM,uBAAuB,CAAC;AAE1D,OAAO,KAAK,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,aAAa,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AA8DlI,KAAK,WAAW,GAAG;IAAE,IAAI,EAAE,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAG9E,OAAO,KAAK,EAAE,QAAQ,EAAsD,MAAM,0BAA0B,CAAC;AAsC7G,KAAK,eAAe,GAAG;IACnB,SAAS,EAAE,eAAe,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,UAAU,CAAC;IACnB,UAAU,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7C,CAAC;AAEF,KAAK,cAAc,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,CAAC;AAOjF,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAC9D,MAAM,WAAW,kBAAkB;IAC/B,QAAQ,EAAE,gBAAgB,CAAC;IAK3B,IAAI,CAAC,EAAE,MAAM,CAAC;IAKd,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAYD,MAAM,WAAW,oBAAoB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,SAAS,CAAC;IAIjB,gBAAgB,EAAE,OAAO,CAAC;CAC7B;AA0GD,MAAM,CAAC,OAAO,OAAO,MAAM;;IACvB,MAAM,CAAC,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAUhF,MAAM,CAAC,eAAe,CAAC,GAAG,EAAE,aAAa,CAAC,eAAe,CAAC,GAAG,MAAM;IAQnE,MAAM,CAAC,WAAW,CACd,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,EAC9B,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,GACvB;QAAE,QAAQ,EAAE,KAAK,CAAA;KAAE,GAAG;QAAE,QAAQ,EAAE,IAAI,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE;gBAwE/D,EAAE,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,aAAa,EAAE,SAAS,EAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,EAAE,EAAE;QAC5H,EAAE,EAAE,EAAE,CAAC;QACP,OAAO,EAAE,cAAc,CAAC;QACxB,SAAS,CAAC,EAAE,SAAS,CAAC;QACtB,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;QACtC,aAAa,CAAC,EAAE,aAAa,CAAC;QAC9B,SAAS,CAAC,EAAE,eAAe,CAAC;QAC5B,SAAS,CAAC,EAAE,eAAe,CAAC;QAC5B,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;QAC5C,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;KACvC;IAwBD,YAAY,CAAC,SAAS,EAAE,gBAAgB,GAAG,IAAI;IA6BzC,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC;IAmD/H,OAAO,CAAC,EACV,QAAQ,EAAE,QAAQ,EAAE,YAAiB,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAC/D,QAAa,EAAE,UAA6B,EAC5C,SAAoE,EACpE,cAAqF,EACrF,MAAgB,EAAE,MAAM,EAAE,UAAU,GACvC,EAAE;QACC,QAAQ,EAAE,QAAQ,CAAC;QACnB,QAAQ,EAAE,WAAW,EAAE,CAAC;QAIxB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QACjD,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,MAAM,CAAC,EAAE,UAAU,CAAC;QACpB,MAAM,CAAC,EAAE,WAAW,CAAC;QACrB,UAAU,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;KAC7C,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,WAAW,GAAG,kBAAkB,GAAG,iBAAiB,GAAG,UAAU,GAAG,IAAI,CAAA;KAAE,CAAC;IAqIzJ,OAAO,CAAC,EACV,QAAQ,EAAE,QAAQ,EAAE,YAAiB,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,MAAgB,EAAE,MAAM,EAAE,UAAU,EACrG,UAAc,EAAE,QAAa,GAChC,EAAE;QACC,QAAQ,EAAE,QAAQ,CAAC;QACnB,QAAQ,EAAE,WAAW,EAAE,CAAC;QACxB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QACjD,MAAM,CAAC,EAAE,UAAU,CAAC;QACpB,MAAM,CAAC,EAAE,WAAW,CAAC;QACrB,UAAU,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;QAK1C,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACrB,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,OAAO,CAAC;QAAC,cAAc,EAAE,OAAO,CAAA;KAAE,CAAC;IAmnBxI,UAAU,IAAI,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAkPhD,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC;IA6LjE,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,kBAAkB,GAAG,IAAI;IAYzE,kBAAkB,IAAI,MAAM,EAAE;IAQxB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAiBpD,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAChD;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAC7C;IAgCD,iBAAiB,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,IAAI;CAghB3E"}
1
+ {"version":3,"file":"Engine.d.ts","sourceRoot":"","sources":["../../src/core/Engine.ts"],"names":[],"mappings":"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"}
@@ -442,6 +442,8 @@ class Engine {
442
442
  // SPEC §grinder: a non-soft grinder fire counts toward the strike streak.
443
443
  if (turn.budgetStruck)
444
444
  state.turnErrors++; // a grinder fire bumps the strike streak — §grinder-strike-coupling
445
+ if (turn.steerStruck)
446
+ state.turnErrors++; // idle / premature-terminate steer struck — §send the terminal contract
445
447
  this.#strikeState.set(loopId, state);
446
448
  // Rail #38: strike accounting. Three sources strike a turn:
447
449
  // 1. recordedFailed — any action-entry at hard failure status
@@ -616,15 +618,33 @@ class Engine {
616
618
  // one row per scheme that has entries (scheme=null → file). log:// is absent —
617
619
  // it lives in log_entries, not the catalog (present-mode, the # Log section).
618
620
  const catalogSchemes = await this.#db.engine_scheme_catalog_summary.all({ session_id: sessionId });
619
- for (const { scheme, entries } of catalogSchemes) {
621
+ // known:/// + unknown:/// ALWAYS foist, even at zero entries else the model
622
+ // burns a turn running FIND(known:///**) itself, assuming its memory is merely
623
+ // being withheld. Every other scheme keeps the with-entries default (an empty
624
+ // catalog foist is noise for schemes the model isn't expected to pre-populate).
625
+ const foistSchemes = [...catalogSchemes];
626
+ for (const always of ["known", "unknown"]) {
627
+ if (!foistSchemes.some((c) => c.scheme === always))
628
+ foistSchemes.push({ scheme: always, entries: 0 });
629
+ }
630
+ for (const { scheme, entries } of foistSchemes) {
620
631
  const schemeName = scheme ?? "file";
621
- const cap = manifestItems < 0 ? null : Math.min(manifestItems, entries);
632
+ // plurnk its docs subtree (FIND(plurnk://docs/**), uncapped) the self-
633
+ // documenting surface. The prompt is shown in # Prompt, so the plurnk catalog
634
+ // the model orients on IS the docs; doc links are no longer rendered inline (#270).
635
+ const isPlurnk = schemeName === "plurnk";
636
+ // entries===0 (an always-foisted empty known/unknown) → uncapped: nothing to
637
+ // clamp, and Math.min(items, 0)=0 would emit a degenerate <1,0> marker.
638
+ const cap = isPlurnk || manifestItems < 0 || entries === 0 ? null : Math.min(manifestItems, entries);
622
639
  const catalogFind = {
623
640
  op: "FIND", suffix: "", signal: null,
624
641
  target: {
625
- kind: "url", raw: `${schemeName}:///**`, scheme: schemeName,
642
+ kind: "url",
643
+ raw: isPlurnk ? "plurnk://docs/**" : `${schemeName}:///**`,
644
+ scheme: schemeName,
626
645
  username: null, password: null, hostname: null, port: null,
627
- pathname: "", params: {}, fragment: null,
646
+ pathname: isPlurnk ? "/docs/**" : "/**",
647
+ params: {}, fragment: null,
628
648
  },
629
649
  body: null,
630
650
  lineMarker: cap === null ? null : { marks: [1, cap] },
@@ -686,10 +706,13 @@ class Engine {
686
706
  nextActionIndex++;
687
707
  }
688
708
  }
689
- // §env-delta — pre-seed environment deltas (changes since this run last
690
- // reconciled) as system EDIT rows, before the packet composes; advance
691
- // the action index past them so model ops continue after.
709
+ // §environment-observation — pre-seed the run's ambient observations (what changed since
710
+ // it last looked) as foisted rows before the packet composes; advance the action index
711
+ // past them so model ops continue after. Two instances of one machine: env-delta (sibling
712
+ // edits · timestamp cursor · always folded) and exec streams (channel bytes · byte cursor ·
713
+ // terminal delta opens). §env-delta §exec-stream
692
714
  nextActionIndex += await this.#materializeEnvironmentDeltas({ sessionId, runId, loopId, turnId, fromSequence: nextActionIndex });
715
+ nextActionIndex += await this.#materializeStreamDeltas({ runId, loopId, turnId, fromSequence: nextActionIndex });
693
716
  // SPEC §telemetry — git working-tree state for the telemetry section, read once
694
717
  // (a service-side `git status` shell-out) and threaded into the budget
695
718
  // rebuild too so it isn't re-shelled on overflow.
@@ -719,7 +742,7 @@ class Engine {
719
742
  usage_prompt: 0, usage_completion: 0, usage_cached: 0, usage_cost_pico: 0,
720
743
  finish_reason: "budget_hard_stop", model: provider.model,
721
744
  });
722
- return { turnId, status: 413, statuses: [], fingerprint: "", budgetStruck: enforced.struck, budgetHardStop: true };
745
+ return { turnId, status: 413, statuses: [], fingerprint: "", budgetStruck: enforced.struck, budgetHardStop: true, steerStruck: false };
723
746
  }
724
747
  const modelMessages = this.#packetToWireMessages(requestPacket);
725
748
  // maxTokens = remaining context window (loop policy, plurnk-providers#10).
@@ -794,12 +817,43 @@ class Engine {
794
817
  // as no-ops, and the terminal scan ignores 1xx so they never set turnStatus.
795
818
  const realOpsCount = packetAssistant.ops.filter((op) => op.op !== "PLAN" && !(op.op === "SEND" && op.signal === 103 && op.target === null)).length;
796
819
  const sendOp = packetAssistant.ops.findLast((op) => op.op === "SEND" && typeof op.signal === "number" && op.signal >= 200);
797
- // Rail #41 (revised): the per-turn requirement is "emit at least one
798
- // op," not "emit a terminal SEND." SEND is purely a signal verb; many
799
- // turns may pass without one. An empty op list is the only strike.
820
+ // §send the terminal contract two engine error states verify a terminal claim against run
821
+ // state, never trusting the model's code. Both strike via turn.steerStruck (turnErrors,
822
+ // §grinder-strike-coupling): the loop continues, the model sees the steering hint not the strike
823
+ // count, and a non-resolver spins out to the engine's 500.
824
+ let steerStruck = false;
825
+ // Premature terminate: a SEND[200] while the run still holds a live stream/spawn — the model
826
+ // declared done with work running. Downgrade the 200 to 102 so it dispatches as a continue (its
827
+ // body is preserved, not discarded) and steer; the stream's own conclusion or a KILL is the exit.
828
+ if (sendOp?.signal === 200) {
829
+ const openSubs = await this.#db.find_open_subscriptions_for_run.all({ run_id: runId });
830
+ const execHandler = this.#schemes.get("exec");
831
+ if (openSubs.length > 0 || execHandler?.hasActiveSpawns?.(runId) === true) {
832
+ sendOp.signal = TURN_STATUS_IMPLICIT_CONTINUE; // 102 — downgraded, no longer a terminal
833
+ steerStruck = true;
834
+ this.#pushTelemetry(sessionId, loopId, {
835
+ source: "engine:rail",
836
+ kind: "premature_terminate",
837
+ message: "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.",
838
+ });
839
+ }
840
+ }
841
+ // Rail #41 (revised): the per-turn requirement is "emit at least one op," not "emit a terminal
842
+ // SEND." SEND is purely a signal verb; many turns pass without one. An empty op list strikes.
800
843
  const turnStatus = sendOp !== undefined
801
844
  ? sendOp.signal
802
845
  : realOpsCount === 0 ? TURN_STATUS_NO_OPS : TURN_STATUS_IMPLICIT_CONTINUE;
846
+ // Idle turn: an implicit-continue (102) that did no WORK — its ops are only PLAN/SEND, no mid op.
847
+ // The model continued with nothing to do. (Skipped when premature already steered this turn.)
848
+ const midOpsCount = packetAssistant.ops.filter((op) => op.op !== "PLAN" && op.op !== "SEND").length;
849
+ if (!steerStruck && turnStatus === TURN_STATUS_IMPLICIT_CONTINUE && midOpsCount === 0) {
850
+ steerStruck = true;
851
+ this.#pushTelemetry(sessionId, loopId, {
852
+ source: "engine:rail",
853
+ kind: "idle_turn",
854
+ message: "If the turn's work is complete, terminate with 200. If awaiting a stream or run trigger, terminate with 202 to hibernate.",
855
+ });
856
+ }
803
857
  // Close the turn with the final packet, status, and usage stats.
804
858
  const packet = this.#completePacket(requestPacket, packetAssistant, response.assistantRaw, provider);
805
859
  const { usage, finishReason, model } = callMetadata;
@@ -860,7 +914,7 @@ class Engine {
860
914
  // nothing. Strike accounting (engine-internal) treats it as a
861
915
  // struck turn; the model just sees an empty packet next turn.
862
916
  // Per SPEC §telemetry gamification policy.
863
- return { turnId, status: turnStatus, statuses, fingerprint: _a.fingerprintTurn(packetAssistant.ops), budgetStruck: enforced.struck, budgetHardStop: false };
917
+ return { turnId, status: turnStatus, statuses, fingerprint: _a.fingerprintTurn(packetAssistant.ops), budgetStruck: enforced.struck, budgetHardStop: false, steerStruck };
864
918
  }
865
919
  // Split the wire-level ProviderResponse into the two destinations:
866
920
  // packet.assistant gets the model's emission (content, ops, reasoning);
@@ -952,11 +1006,11 @@ class Engine {
952
1006
  // requirements). Read Paths.defaultRequirements (PLURNK_REQUIREMENTS env →
953
1007
  // requirements.md) fresh each build so edits take effect; a non-empty param wins.
954
1008
  const baseRequirements = requirements.length > 0 ? requirements : await readFile(Paths.defaultRequirements, "utf8");
955
- // The op syntax leads the requirements. PLAN is mandated unconditionally by
956
- // plurnk.md §Imperatives (grammar 0.70 requires every turn to lead with PLAN),
957
- // so the service injects no separate plan directive here the former PLURNK_PLAN
958
- // gating is retired (PLURNK_PLAN is no longer a flag).
959
- const requirementsText = `Syntax: <<OPsuffix[signal]?(target)?<Line/Result>?:body?:OPsuffix\n\n${baseRequirements}`;
1009
+ // No injected syntax line: the grammar already headlines the system definition (§Syntax) and
1010
+ // leads requirements.md, so a third copy here was pure duplication in the model's packet. PLAN
1011
+ // is mandated unconditionally by plurnk.md §Imperatives (grammar 0.70 requires every turn to
1012
+ // lead with PLAN), so the service injects no separate plan directive either (the former
1013
+ // PLURNK_PLAN gating is retired — PLURNK_PLAN is no longer a flag).
960
1014
  const log = await this.#buildLog(runId);
961
1015
  const telemetryErrors = presetTelemetry ?? await this.#buildTelemetryErrors(loopId, currentTurnSeq);
962
1016
  const countTokens = (t) => provider.countTokens(t); // §provider-surface-counttokens
@@ -970,9 +1024,6 @@ class Engine {
970
1024
  // omitted, section lines still shown). §tokenomics-render-weight-budget
971
1025
  const ceiling = _a.computeCeiling(provider.contextSize, this.#budgetCeiling);
972
1026
  const budgetReadout = this.#renderBudget(PacketWire.measureLogBudget(log, countTokens), ceiling);
973
- // Per-scheme tally (§packet) so the model sees which schemes hold content without
974
- // probing e.g. FIND(known://**) every turn. "" when empty → the section is omitted.
975
- const catalogSummary = await this.#db.engine_scheme_catalog_summary.all({ session_id: sessionId });
976
1027
  // The default packet: an ordered list of sections, each addressable state
977
1028
  // (§packet-construction). `slot` is the prompt-cache boundary; the STATIC
978
1029
  // sections (definition, tools) lead the system slot so they form the cached
@@ -986,13 +1037,12 @@ class Engine {
986
1037
  { name: "tools", slot: "system", header: null, content: tools.join("\n"), tokens: 0 }, // titleless — the examples flow on from plurnk.md (definition) directly above
987
1038
  { name: "schemes", slot: "system", header: "Plurnk System Schemes", content: this.#schemes.teach(), tokens: 0 },
988
1039
  ...(inject !== null ? [{ name: "inject", slot: "system", header: "Plurnk Operator Notes", content: inject, tokens: 0 }] : []),
989
- { name: "log", slot: "system", header: "Plurnk System Log", content: PacketWire.renderLog(log), tokens: 0 },
1040
+ { name: "log", slot: "system", header: "Plurnk System Log", content: PacketWire.renderLog(log, countTokens), tokens: 0 },
990
1041
  { name: "prompt", slot: "user", header: "Plurnk System User Prompt", content: prompt, tokens: 0 },
991
1042
  { name: "budget", slot: "user", header: "Plurnk System Budget", content: budgetReadout, tokens: 0 },
992
1043
  { name: "errors", slot: "user", header: "Plurnk System Errors", content: PacketWire.renderErrors(telemetryErrors), tokens: 0 },
993
1044
  { name: "git", slot: "user", header: "Plurnk System Git Status", content: PacketWire.renderGit(gitStatus), tokens: 0 },
994
- { name: "catalog", slot: "user", header: "Plurnk System Catalog", content: PacketWire.renderCatalog(catalogSummary), tokens: 0 },
995
- { name: "requirements", slot: "user", header: "Plurnk System Requirements", content: requirementsText, tokens: 0 },
1045
+ { name: "requirements", slot: "user", header: "Plurnk System Requirements", content: baseRequirements, tokens: 0 },
996
1046
  ];
997
1047
  // Plugin packet control (§packet-construction): trusted schemes rewrite the
998
1048
  // default list — add, remove, reorder — in-process, before measurement.
@@ -1018,20 +1068,22 @@ class Engine {
1018
1068
  const packetTokens = countTokens(PacketWire.renderSlot(sections, "system")) + countTokens(PacketWire.renderSlot(sections, "user"));
1019
1069
  return { tokens: packetTokens, sections, telemetryErrors };
1020
1070
  }
1021
- // Budget readout body, rendered into the `# Plurnk System Budget` section.
1071
+ // Budget readout body, rendered into the `## Plurnk System Budget` section.
1022
1072
  // Headline `ceiling/free` only when a ceiling exists; section lines for the
1023
1073
  // curatable index/log weight the model can FOLD back. tokensFree is a
1024
1074
  // placeholder here — buildSystem substitutes it after measuring the packet.
1025
1075
  #renderBudget(log, ceiling) {
1026
1076
  const lines = [];
1027
1077
  if (ceiling !== null)
1028
- lines.push(`ceiling ${ceiling} · usage ${TOKEN_USAGE_PLACEHOLDER} (${TOKEN_PERCENT_PLACEHOLDER}%) · free ${TOKENS_FREE_PLACEHOLDER}`);
1078
+ lines.push(`Token Ceiling ${ceiling} · Token Usage ${TOKEN_USAGE_PLACEHOLDER} (${TOKEN_PERCENT_PLACEHOLDER}%) · Tokens Free ${TOKENS_FREE_PLACEHOLDER}`);
1029
1079
  if (log.entries > 0) {
1080
+ if (lines.length > 0)
1081
+ lines.push("");
1030
1082
  lines.push(`Log entries: ${log.entries} entries, ${log.tokens} tokens`);
1031
1083
  // Per-turn weight — the grinder's rollback unit, oldest first: the
1032
1084
  // model sees what's first to go (§tokenomics {§tokenomics-turn-totals}).
1033
1085
  if (log.byTurn.length > 0) {
1034
- lines.push("Turns:", "| turn | tokens |", "|---|--:|");
1086
+ lines.push("", "Turns:", "| turn | tokens |", "|---|--:|");
1035
1087
  for (const t of log.byTurn)
1036
1088
  lines.push(`| ${t.turn} | ${t.tokens} |`);
1037
1089
  }
@@ -1040,14 +1092,14 @@ class Engine {
1040
1092
  // lists log:/// rows (log items), distinct from catalog entries (plurnk.md: "EDIT
1041
1093
  // is only for entries. Do not attempt to edit log items.").
1042
1094
  if (log.largest.length > 0) {
1043
- lines.push("Heaviest items:", "| item | tokens |", "|---|--:|");
1095
+ lines.push("", "Heaviest items:", "| item | tokens |", "|---|--:|");
1044
1096
  for (const e of log.largest)
1045
1097
  lines.push(`| ${e.path} | ${e.tokens} |`);
1046
1098
  }
1047
1099
  }
1048
1100
  return lines.join("\n");
1049
1101
  }
1050
- // The # Plurnk System Tools capability sheet (SPEC §tools). A hook: each enabled
1102
+ // The ## Plurnk System Tools capability sheet (SPEC §tools). A hook: each enabled
1051
1103
  // capability contributes one line, rendered above Requirements so the model sees what
1052
1104
  // it can do before the rules. Each available executor tag contributes its self-documenting
1053
1105
  // example (plurnk-execs#7), retiring the blind EXEC.
@@ -1070,7 +1122,7 @@ class Engine {
1070
1122
  // the fuller doc (materialized at plurnk://docs/<tag>.md) rides an inline link whose
1071
1123
  // token cost lives on that manifest entry. No example → no line (like a provisional scheme).
1072
1124
  if (entry?.example)
1073
- tools.push(teachingLine(entry.example, tag, Boolean(entry.documentation)));
1125
+ tools.push(teachingLine(entry.example));
1074
1126
  }
1075
1127
  }
1076
1128
  return tools;
@@ -1221,6 +1273,7 @@ class Engine {
1221
1273
  mimetype_tx: r.mimetype_tx,
1222
1274
  folded: r.expanded === 0,
1223
1275
  source: r.source,
1276
+ attrs: r.attrs === null ? null : JSON.parse(r.attrs),
1224
1277
  }));
1225
1278
  }
1226
1279
  // §env-delta (§actor-boundary-no-mutex: runs share without locks; a conflict surfaces as a delta, never prevented) — at pre-turn build, surface what changed in the shared world since this
@@ -1261,6 +1314,37 @@ class Engine {
1261
1314
  }
1262
1315
  return written;
1263
1316
  }
1317
+ // §environment-observation — exec streams as an instance of the ambient-observe machine:
1318
+ // each turn, emit each owned channel's unshown byte-delta as a foisted READ@200 row. Folded
1319
+ // while the channel streams; the terminal delta (channel closed) auto-OPENs. The cursor is the
1320
+ // streamEnd recorded on the channel's prior delta — no exec-specific surfacing, just the
1321
+ // env-observe loop with a byte cursor where env-delta uses a timestamp. §exec-stream
1322
+ async #materializeStreamDeltas(args) {
1323
+ const { runId, loopId, turnId, fromSequence } = args;
1324
+ const channels = await this.#db.engine_run_stream_channels.all({ run_id: runId });
1325
+ let written = 0;
1326
+ for (const ch of channels) {
1327
+ const prior = await this.#db.engine_stream_cursor.get({
1328
+ run_id: runId, scheme: ch.runtime, pathname: ch.coord, fragment: ch.channel,
1329
+ });
1330
+ const cursor = prior !== undefined ? (JSON.parse(prior.attrs).streamEnd ?? 0) : 0;
1331
+ if (ch.content.length <= cursor)
1332
+ continue; // nothing new to show this turn
1333
+ const closed = ch.state === "closed" || ch.state === "errored";
1334
+ // startLine continues the line count across turns: a multi-turn stream's deltas number
1335
+ // into one sequence (lines N..M, then M+1..), not N independent "1:" restarts. §exec-stream
1336
+ const startLine = (ch.content.slice(0, cursor).match(/\n/g)?.length ?? 0) + 1;
1337
+ await this.#db.engine_insert_stream_delta.run({
1338
+ run_id: runId, loop_id: loopId, turn_id: turnId, sequence: fromSequence + written,
1339
+ scheme: ch.runtime, pathname: ch.coord, fragment: ch.channel,
1340
+ rx: JSON.stringify({ status: 200, content: ch.content.slice(cursor), mimetype: "text/stream", startLine }),
1341
+ attrs: JSON.stringify({ streamEnd: ch.content.length }),
1342
+ expanded: closed ? 1 : 0, // §exec-stream — terminal delta auto-OPENs; ongoing folds
1343
+ });
1344
+ written++;
1345
+ }
1346
+ return written;
1347
+ }
1264
1348
  // §env-delta — the filesystem as an actor. Ambient disk divergences detected at
1265
1349
  // pre-turn (git membership re-read) are logged as the plurnk run's source=file EDIT
1266
1350
  // "fictions": no op happened, but EDIT is the only grammar the model has for "your
@@ -1996,12 +2080,12 @@ class Engine {
1996
2080
  let attrsObj = (result.attrs !== undefined && result.attrs !== null)
1997
2081
  ? { ...result.attrs }
1998
2082
  : {};
1999
- // EXEC stream entry addresses by RUNTIME TAG as authority (§exec): it lives at
2000
- // <runtime>:///<loop_seq>/<turn_seq>/<sequence> (e.g. sh:///1/1/2) the runtime tag
2001
- // is the scheme, the coordinate already unique per statement. The log row's target
2002
- // points at this same address; its log:/// coordinate shares the trailing
2003
- // <loop>/<turn>/<seq>, so the model correlates op to stream output. Runtime comes
2004
- // from statement.signal (EXEC's runtime slot) so it's resolvable for failed execs
2083
+ // EXEC produces a stream entry addressed by RUNTIME TAG as authority (§exec): it lives
2084
+ // at <runtime>:///<loop_seq>/<turn_seq>/<sequence> (e.g. sh:///1/1/2). That address is a
2085
+ // SEPARATE `stream` link in attrs NOT an overload of `target`, which stays faithful to
2086
+ // the EXEC's own slot (the cwd, or the path to the executable). The log:/// coordinate
2087
+ // shares the trailing <loop>/<turn>/<seq>, so the op still correlates to its stream.
2088
+ // Runtime comes from statement.signal (EXEC's runtime slot), resolvable for failed execs
2005
2089
  // too; empty/absent = the default shell.
2006
2090
  if (statement.op === "EXEC") {
2007
2091
  const seqs = await this.#db.engine_loop_turn_seqs.get({
@@ -2011,9 +2095,8 @@ class Engine {
2011
2095
  throw new Error(`Engine.#writeLog: loop_turn_seqs returned no row for loop=${loopId} turn=${turnId}`);
2012
2096
  const runtime = (typeof statement.signal === "string" && statement.signal.length > 0) ? statement.signal : "sh";
2013
2097
  const coordPathname = `/${seqs.loop_seq}/${seqs.turn_seq}/${sequence}`;
2014
- target.scheme = runtime;
2015
- target.pathname = coordPathname;
2016
2098
  attrsObj.pathname = coordPathname;
2099
+ attrsObj.stream = `${runtime}://${coordPathname}`;
2017
2100
  // Mutate the in-memory result.attrs too: the dispatch path
2018
2101
  // hands originalResult.attrs to handler.applyResolution after
2019
2102
  // proposal accept (see #acceptResolution). Both views — the