@plurnk/plurnk-service 1.3.2 → 1.3.4
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.
- package/.env.defaults +16 -3
- package/INSTALL.md +7 -3
- package/README.md +9 -0
- package/SPEC.md +82 -34
- package/dist/build-info.json +1 -1
- package/dist/content/edit-receipt.d.ts +35 -0
- package/dist/content/edit-receipt.d.ts.map +1 -0
- package/dist/content/edit-receipt.js +158 -0
- package/dist/content/edit-receipt.js.map +1 -0
- package/dist/content/index.d.ts +2 -0
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +1 -0
- package/dist/content/index.js.map +1 -1
- package/dist/content/line-marker.d.ts +3 -1
- package/dist/content/line-marker.d.ts.map +1 -1
- package/dist/content/line-marker.js +2 -0
- package/dist/content/line-marker.js.map +1 -1
- package/dist/core/ChannelWrite.d.ts +2 -1
- package/dist/core/ChannelWrite.d.ts.map +1 -1
- package/dist/core/ChannelWrite.js +7 -6
- package/dist/core/ChannelWrite.js.map +1 -1
- package/dist/core/CoreSchemeServices.d.ts +31 -0
- package/dist/core/CoreSchemeServices.d.ts.map +1 -0
- package/dist/core/CoreSchemeServices.js +37 -0
- package/dist/core/CoreSchemeServices.js.map +1 -0
- package/dist/core/Db.d.ts +2 -13
- package/dist/core/Db.d.ts.map +1 -1
- package/dist/core/Db.js +0 -5
- package/dist/core/Db.js.map +1 -1
- package/dist/core/Dispatcher.d.ts +7 -3
- package/dist/core/Dispatcher.d.ts.map +1 -1
- package/dist/core/Dispatcher.js +266 -70
- package/dist/core/Dispatcher.js.map +1 -1
- package/dist/core/Engine.d.ts +4 -2
- package/dist/core/Engine.d.ts.map +1 -1
- package/dist/core/Engine.js +55 -37
- package/dist/core/Engine.js.map +1 -1
- package/dist/core/Engine.sql +26 -40
- package/dist/core/LiveSubscriptions.d.ts +8 -0
- package/dist/core/LiveSubscriptions.d.ts.map +1 -0
- package/dist/core/LiveSubscriptions.js +32 -0
- package/dist/core/LiveSubscriptions.js.map +1 -0
- package/dist/core/LoopLifecycle.d.ts +20 -0
- package/dist/core/LoopLifecycle.d.ts.map +1 -0
- package/dist/core/LoopLifecycle.js +56 -0
- package/dist/core/LoopLifecycle.js.map +1 -0
- package/dist/core/LoopLifecycle.sql +60 -0
- package/dist/core/Owner.d.ts.map +1 -1
- package/dist/core/Owner.js.map +1 -1
- package/dist/core/PacketBuilder.d.ts.map +1 -1
- package/dist/core/PacketBuilder.js.map +1 -1
- package/dist/core/ProposalLifecycle.d.ts +4 -1
- package/dist/core/ProposalLifecycle.d.ts.map +1 -1
- package/dist/core/ProposalLifecycle.js +12 -6
- package/dist/core/ProposalLifecycle.js.map +1 -1
- package/dist/core/ProviderInstantiate.d.ts.map +1 -1
- package/dist/core/ProviderInstantiate.js +10 -22
- package/dist/core/ProviderInstantiate.js.map +1 -1
- package/dist/core/SchemeRegistry.d.ts +3 -0
- package/dist/core/SchemeRegistry.d.ts.map +1 -1
- package/dist/core/SchemeRegistry.js +23 -0
- package/dist/core/SchemeRegistry.js.map +1 -1
- package/dist/core/TelemetryChannel.d.ts.map +1 -1
- package/dist/core/TelemetryChannel.js.map +1 -1
- package/dist/core/caps/CapsResolve.d.ts.map +1 -1
- package/dist/core/caps/CapsResolve.js.map +1 -1
- package/dist/core/caps/DbChannelCaps.d.ts.map +1 -1
- package/dist/core/caps/DbChannelCaps.js.map +1 -1
- package/dist/core/caps/DbEntryCaps.d.ts.map +1 -1
- package/dist/core/caps/DbEntryCaps.js +8 -4
- package/dist/core/caps/DbEntryCaps.js.map +1 -1
- package/dist/core/caps/DbSubscriptionCaps.d.ts +3 -2
- package/dist/core/caps/DbSubscriptionCaps.d.ts.map +1 -1
- package/dist/core/caps/DbSubscriptionCaps.js +20 -9
- package/dist/core/caps/DbSubscriptionCaps.js.map +1 -1
- package/dist/core/caps/DbTagCaps.d.ts.map +1 -1
- package/dist/core/caps/DbTagCaps.js.map +1 -1
- package/dist/core/caps/SchemeCtxImpl.d.ts +3 -16
- package/dist/core/caps/SchemeCtxImpl.d.ts.map +1 -1
- package/dist/core/caps/SchemeCtxImpl.js +4 -23
- package/dist/core/caps/SchemeCtxImpl.js.map +1 -1
- package/dist/core/fork.d.ts.map +1 -1
- package/dist/core/fork.js.map +1 -1
- package/dist/core/git-iso.d.ts +3 -0
- package/dist/core/git-iso.d.ts.map +1 -1
- package/dist/core/git-iso.js +35 -4
- package/dist/core/git-iso.js.map +1 -1
- package/dist/core/git-membership.d.ts.map +1 -1
- package/dist/core/git-membership.js.map +1 -1
- package/dist/core/git-state.d.ts.map +1 -1
- package/dist/core/git-state.js +3 -6
- package/dist/core/git-state.js.map +1 -1
- package/dist/core/packet-wire.d.ts.map +1 -1
- package/dist/core/packet-wire.js +37 -11
- package/dist/core/packet-wire.js.map +1 -1
- package/dist/core/run-ops.sql +1 -1
- package/dist/core/scheme-types.d.ts.map +1 -1
- package/dist/core/scheme-types.js +3 -4
- package/dist/core/scheme-types.js.map +1 -1
- package/dist/core/turn-scheduler.d.ts +3 -0
- package/dist/core/turn-scheduler.d.ts.map +1 -0
- package/dist/core/turn-scheduler.js +20 -0
- package/dist/core/turn-scheduler.js.map +1 -0
- package/dist/core/worker-cap.d.ts.map +1 -1
- package/dist/core/worker-cap.js.map +1 -1
- package/dist/core/workspace-settings.d.ts.map +1 -1
- package/dist/core/workspace-settings.js.map +1 -1
- package/dist/core/world-state.d.ts.map +1 -1
- package/dist/core/world-state.js.map +1 -1
- package/dist/digest/Digest.d.ts.map +1 -1
- package/dist/digest/Digest.js +4 -6
- package/dist/digest/Digest.js.map +1 -1
- package/dist/schemes/Exec.d.ts +11 -10
- package/dist/schemes/Exec.d.ts.map +1 -1
- package/dist/schemes/Exec.js +45 -38
- package/dist/schemes/Exec.js.map +1 -1
- package/dist/schemes/ExecOutputScheme.d.ts +8 -6
- package/dist/schemes/ExecOutputScheme.d.ts.map +1 -1
- package/dist/schemes/ExecOutputScheme.js +11 -6
- package/dist/schemes/ExecOutputScheme.js.map +1 -1
- package/dist/schemes/File.d.ts +16 -9
- package/dist/schemes/File.d.ts.map +1 -1
- package/dist/schemes/File.js +75 -26
- package/dist/schemes/File.js.map +1 -1
- package/dist/schemes/Log.d.ts +9 -7
- package/dist/schemes/Log.d.ts.map +1 -1
- package/dist/schemes/Log.js +21 -9
- package/dist/schemes/Log.js.map +1 -1
- package/dist/schemes/Prompt.d.ts +1 -4
- package/dist/schemes/Prompt.d.ts.map +1 -1
- package/dist/schemes/Prompt.js +2 -7
- package/dist/schemes/Prompt.js.map +1 -1
- package/dist/schemes/Skill.d.ts +1 -5
- package/dist/schemes/Skill.d.ts.map +1 -1
- package/dist/schemes/Skill.js +4 -11
- package/dist/schemes/Skill.js.map +1 -1
- package/dist/schemes/Worker.d.ts +15 -10
- package/dist/schemes/Worker.d.ts.map +1 -1
- package/dist/schemes/Worker.js +40 -29
- package/dist/schemes/Worker.js.map +1 -1
- package/dist/schemes/_entry-crud.d.ts.map +1 -1
- package/dist/schemes/_entry-crud.js.map +1 -1
- package/dist/schemes/_entry-find.d.ts +8 -8
- package/dist/schemes/_entry-find.d.ts.map +1 -1
- package/dist/schemes/_entry-find.js +76 -23
- package/dist/schemes/_entry-find.js.map +1 -1
- package/dist/schemes/_entry-find.sql +20 -0
- package/dist/schemes/_entry-graph.d.ts.map +1 -1
- package/dist/schemes/_entry-graph.js.map +1 -1
- package/dist/schemes/_entry-manifest.d.ts.map +1 -1
- package/dist/schemes/_entry-manifest.js.map +1 -1
- package/dist/schemes/_entry-ops.d.ts +3 -2
- package/dist/schemes/_entry-ops.d.ts.map +1 -1
- package/dist/schemes/_entry-ops.js +41 -13
- package/dist/schemes/_entry-ops.js.map +1 -1
- package/dist/schemes/_entry-semantic.d.ts +2 -1
- package/dist/schemes/_entry-semantic.d.ts.map +1 -1
- package/dist/schemes/_entry-semantic.js +15 -4
- package/dist/schemes/_entry-semantic.js.map +1 -1
- package/dist/schemes/_entry-semantic.sql +3 -0
- package/dist/schemes/_entry-send.d.ts.map +1 -1
- package/dist/schemes/_entry-send.js.map +1 -1
- package/dist/server/Daemon.d.ts +9 -1
- package/dist/server/Daemon.d.ts.map +1 -1
- package/dist/server/Daemon.js +161 -74
- package/dist/server/Daemon.js.map +1 -1
- package/dist/server/DaemonLock.d.ts +7 -0
- package/dist/server/DaemonLock.d.ts.map +1 -0
- package/dist/server/DaemonLock.js +85 -0
- package/dist/server/DaemonLock.js.map +1 -0
- package/dist/server/clientTurn.d.ts.map +1 -1
- package/dist/server/clientTurn.js +2 -3
- package/dist/server/clientTurn.js.map +1 -1
- package/dist/server/dispatch-as-plurnk.d.ts.map +1 -1
- package/dist/server/dispatch-as-plurnk.js +20 -2
- package/dist/server/dispatch-as-plurnk.js.map +1 -1
- package/dist/server/drain.sql +4 -8
- package/dist/server/envelope.d.ts.map +1 -1
- package/dist/server/envelope.js +2 -3
- package/dist/server/envelope.js.map +1 -1
- package/dist/server/lifecycle-recovery.sql +68 -0
- package/dist/server/logEntry.d.ts.map +1 -1
- package/dist/server/logEntry.js.map +1 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +61 -40
- package/dist/service.js.map +1 -1
- package/migrations/0000-00-00.01_schema.sql +5 -5
- package/package.json +21 -18
package/.env.defaults
CHANGED
|
@@ -110,11 +110,12 @@ PLURNK_SERVICE_GIT_AUTO=1
|
|
|
110
110
|
# PLURNK_SERVICE_GIT_NATIVE — git READ backend. Default (0) = in-process isomorphic-git:
|
|
111
111
|
# portable, sandbox-safe, no subprocess, hermetic by construction. =1 routes membership +
|
|
112
112
|
# status to the system git binary — in-process membership measures ~8x native (~130ms at
|
|
113
|
-
# 20k files) and the status read ~55x
|
|
113
|
+
# 20k files) and the status read ~55x. Native is also the explicit remedy when
|
|
114
|
+
# isomorphic-git cannot read a repository shape; it requires the git binary.
|
|
114
115
|
PLURNK_SERVICE_GIT_NATIVE=0
|
|
115
116
|
|
|
116
117
|
# PLURNK_SERVICE_ARRIVAL_PREVIEW_LINES — the arrival law (§arrival-law, #499): content another
|
|
117
|
-
# actor pushes at a worker (a prompt, a child's deliverable) rides OPEN only up to this many
|
|
118
|
+
# actor pushes at a worker (a prompt, a child's deliverable, terminal stream output) rides OPEN only up to this many
|
|
118
119
|
# lines AND 80×N chars (the char cap guards single-line bombs); over, a bounded head + the
|
|
119
120
|
# address + the full price ride instead. Self-invited content (a worker's own READ/FIND) is
|
|
120
121
|
# budget-bounded, never previewed. 16 = generous but not dangerous.
|
|
@@ -140,6 +141,15 @@ PLURNK_SERVICE_ARRIVAL_PREVIEW_LINES=16
|
|
|
140
141
|
# settings.filesItems (#231).
|
|
141
142
|
PLURNK_SERVICE_FILES_ITEMS=-1
|
|
142
143
|
|
|
144
|
+
# Number of full SHA-256 revision characters rendered on each model-facing
|
|
145
|
+
# EDIT receipt. The durable operation result retains all 64 characters; this
|
|
146
|
+
# abbreviation is display correlation only, never identity or comparison.
|
|
147
|
+
PLURNK_SERVICE_EDIT_RECEIPT_REVISION_CHARS=8
|
|
148
|
+
|
|
149
|
+
# Number of neighboring lines (or top-level JSON items) rendered on each side
|
|
150
|
+
# of an EDIT's resulting range. The model can READ for arbitrary final content.
|
|
151
|
+
PLURNK_SERVICE_EDIT_RECEIPT_CONTEXT_LINES=2
|
|
152
|
+
|
|
143
153
|
# Workspace-tier ceiling on CONCURRENT active runs (a run with a non-terminal loop)
|
|
144
154
|
# — the fork-bomb / destabilization brake. -1 = no cap (default); only concurrency
|
|
145
155
|
# is bounded, never lifetime, since workspaces persist for months. A spawn/fork past
|
|
@@ -213,8 +223,11 @@ PLURNK_SERVICE_DOCS_EXCLUDE="plurnk,file,exec"
|
|
|
213
223
|
# --- Semantic search (~query chunking) ---
|
|
214
224
|
# Project Semantics tiles each entry into <=window chunks so a large body is fully
|
|
215
225
|
# searchable, not truncated. ACTIVE only when the installed embedder reports its
|
|
216
|
-
# tokenizer; otherwise one whole-entry chunk (today's behavior).
|
|
226
|
+
# tokenizer; otherwise one whole-entry chunk (today's behavior). The chunk knobs are read
|
|
217
227
|
# only when chunking is active.
|
|
228
|
+
# Ranked results returned by a markerless `FIND(...):~query:FIND`. A positive
|
|
229
|
+
# integer `<K>` overrides this count; a decimal marker selects by similarity.
|
|
230
|
+
PLURNK_SERVICE_SEMANTIC_TOP_K=10
|
|
218
231
|
# Chunk budget in TOKENS. EMPTY (the default) = the installed embedder's reported
|
|
219
232
|
# window — NO model-specific number is assumed, so it scales to whatever embedder you
|
|
220
233
|
# install. Set a positive value ONLY to cap below the window (e.g. to sweep
|
package/INSTALL.md
CHANGED
|
@@ -39,13 +39,17 @@ These are relationships *between* flags. Set them as a unit.
|
|
|
39
39
|
|
|
40
40
|
- **The window partition is exact.** `promptBudget = min(PLURNK_SERVICE_CONTEXT_WINDOW, real window) − REASONING − COMPLETION − SAFETY`; `REASONING + COMPLETION` is the per-call `max_tokens`. Shipped invariant: any window ≥ 77Ki partitions to **exactly 65536** prompt tokens (`78848 − 4096 − 8192 − 1024`). Reserves exceeding the window fail the boot. *(Pinned: `Engine.budget` / `shipped-defaults`.)*
|
|
41
41
|
- **Reasoning capacity is one number in three places.** `PLURNK_SERVICE_REASONING` (the partition's reserve) **must equal** `PLURNK_PROVIDERS_THINKING_CAPACITY` (the provider's thinking cap) **must equal** the serving box's `--reasoning-budget` launch flag. llama-server ignores per-request numeric budgets, so only the launch flag clamps it; a mismatch makes the reserve fiction. The daemon warns at boot when thinking is on. *(Pinned: `shipped-defaults` asserts the first equality.)*
|
|
42
|
-
- **
|
|
42
|
+
- **Local GBNF is optional.** The PLURNK language is always parsed normally.
|
|
43
|
+
Local llama-server users may set `PLURNK_PROVIDERS_GBNF_<alias>`; transport
|
|
44
|
+
and enforcement are verified at boot. Cloud and endpoint-managed aliases
|
|
45
|
+
leave it unset. Pin `PLURNK_PROVIDERS_LLAMA_SERVER_<alias>=1` only when a
|
|
46
|
+
llama-server cannot be fingerprinted reliably.
|
|
43
47
|
- **A think-trained model must think somewhere.** `PLURNK_PROVIDERS_THINKING=off` reroutes a reasoning model's thought into the grammar's legal free zone as prose. Keep it `on` with a capacity; providers auto-clamp thinking on in-band grammar backends, so one setting is right everywhere.
|
|
44
48
|
|
|
45
49
|
## Profiles (examples, not a decision tree — adapt to the real box)
|
|
46
50
|
|
|
47
51
|
- **Local GPU (llama-server).** `PLURNK_MODEL_local="openai/<name>"`, `OPENAI_BASE_URL=http://127.0.0.1:<port>`, `PLURNK_MODEL=local`, `PLURNK_PROVIDERS_LLAMA_SERVER_local=1`, thinking `on`/`4096` **with the box launched `--reasoning-budget 4096`**. Full rails, exact tokenization.
|
|
48
|
-
- **Cloud, bring-your-own-key.** `PLURNK_MODEL_cloud="openrouter/<model>"`, `OPENROUTER_API_KEY=…`, `PLURNK_MODEL=cloud`. No `LLAMA_SERVER` pin
|
|
52
|
+
- **Cloud, bring-your-own-key.** `PLURNK_MODEL_cloud="openrouter/<model>"`, `OPENROUTER_API_KEY=…`, `PLURNK_MODEL=cloud`. No local GBNF or `LLAMA_SERVER` pin.
|
|
49
53
|
- **plurnk.ai endpoint.** `PLURNK_MODEL_plurnk="plurnk/plurnk"`, `PLURNK_API_KEY=…`, `PLURNK_MODEL=plurnk`.
|
|
50
54
|
- **Headless / CI / constrained container.** A CPU-only box should NOT disable semantic search — it should point derivation at a real embedder: `PLURNK_MIMETYPES_EMBED_BASE_URL` (any OpenAI-compatible `/v1/embeddings` — a host GPU turns a CPU-hours corpus grind into seconds). Weak hardware is the target workload, not a reason to shed capability; `PLURNK_SERVICE_EMBED_DISABLE=1` exists for test lanes that deterministically assert non-semantic behavior, nothing else. Consider `PLURNK_SERVICE_MAX_TURNS=<n>` as a cost cap, `PLURNK_SERVICE_GIT_ALLOWED=0` to lock out git in a sandbox.
|
|
51
55
|
|
|
@@ -62,7 +66,7 @@ Each mirrors a `# --- section ---` in the floor; consult the floor for exact def
|
|
|
62
66
|
- **Providers** — `PLURNK_PROVIDERS_THINKING`/`_THINKING_CAPACITY`, `_TEMPERATURE`, `_REPEAT_PENALTY`, `_FETCH_TIMEOUT`, `_RETRY_ATTEMPTS`/`_RETRY_DELAY`, `_PROBE_ATTEMPTS`/`_PROBE_DELAY`, `_GBNF` (grammar variant), `_LLAMA_SERVER_<alias>`, `_CONTEXT_SIZE`, `_GBNF_DEBUG`. Alias-scopable: any knob takes a `_<alias>` suffix that wins over the bare fallback.
|
|
63
67
|
- **The window partition** — `PLURNK_SERVICE_CONTEXT_WINDOW`/`_REASONING`/`_ASSISTANT`/`_SAFETY` (see Couplings).
|
|
64
68
|
- **Plugins** — bare `PLURNK_PLUGINS_TRUSTED_ONLY` (0/unset = load all installed; a value = `@plurnk/*` plus an allowlist).
|
|
65
|
-
- **Semantic search** — `
|
|
69
|
+
- **Semantic search** — `PLURNK_SERVICE_SEMANTIC_TOP_K` (markerless result count), `_SEMANTIC_CHUNK_TOKENS`/`_CHUNK_OVERLAP` (service-side chunking), `PLURNK_SERVICE_EMBED_DISABLE` (FTS-only), `PLURNK_MIMETYPES_EMBED_WORKERS` (the embedder's pool — mimetypes-owned).
|
|
66
70
|
- **Schemes: http** — `PLURNK_SCHEMES_HTTP_FETCH_TIMEOUT`/`_SALVAGE_MIN_BODY_CHARS`/`_IDLE_TIMEOUT` (required on the HTML render path), optional Playwright/Chromium knobs.
|
|
67
71
|
- **Execs** — `PLURNK_EXECS_<runtime>=0` disables a runtime; `PLURNK_EXECS_MCP_<server>` bridges an MCP server; `PLURNK_EXECS_SEARCH_SEARXNG_URL` enables web search (unset = search off).
|
|
68
72
|
|
package/README.md
CHANGED
|
@@ -51,3 +51,12 @@ Plurnk sees what git sees. The model's file surface is defined by three external
|
|
|
51
51
|
Run the root monorepo commands for deterministic lint, unit, and integration
|
|
52
52
|
coverage. Live-model, demo, and installation tests declare their external
|
|
53
53
|
requirements separately.
|
|
54
|
+
|
|
55
|
+
For manual client/service drills, create the modest standalone project used by
|
|
56
|
+
the demos instead of digesting this monorepo:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
npm run fixture:demo -w plurnk-core -- <label>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The command prints the temporary project path. The caller removes it when done.
|
package/SPEC.md
CHANGED
|
@@ -193,7 +193,7 @@ All three ride one engine seam — the daemon's inject (active→fold, idle→en
|
|
|
193
193
|
Beyond the three creation ops:
|
|
194
194
|
|
|
195
195
|
- **Entries (storage)** — entry addressing rides the authority carving above (§worker-authority-carving): `worker:///` the commons, `worker://~/` the own space, a name an ancestry-gated read (§worker-read-scope), writes self-and-commons only (§worker-write-scoping). An entry-path `KILL` deletes under the same write-scoping (200; 404 absent; 403 named) — distinct from the path-ABSENT `KILL(worker://<name>)` which terminates the worker (§worker-scheme-terminate); the discriminator is the entry path, never the op. A worker's own space is catalogued in ITS perspective alone (`FIND(worker://~/**)`, foisted at turn 0 when non-empty); isolation is the owner column, structural.
|
|
196
|
-
- **Terminate** — `KILL(worker://<name>)` aborts a worker by address (self is `worker://self`):
|
|
196
|
+
- **Terminate** — `KILL(worker://<name>)` aborts a worker by address (self is `worker://self`): every unresolved loop in that worker's subtree closes 499 and every subscription in the subtree tears down; a name with no worker is 404. Cancellation is structured: descendants cannot detach implicitly. The override to the fire-and-forget default is not a parent-power — whoever holds the address may end it; a worker left alone simply ends at its own `SEND[200]`. {§worker-scheme-terminate}
|
|
197
197
|
- **Cap** — `PLURNK_SERVICE_WORKSPACE_WORKERS_MAX_ACTIVE` ceilings the *concurrent* active workers per workspace (a worker with a non-terminal loop); a spawn or fork past it fails hard (508 — no queue, no retry), irc exempt; `-1` disables it. The fork-bomb brake, sized for workspaces that live for months. {§worker-scheme-cap}
|
|
198
198
|
- **Collect** — a worker's loop reaching a terminal status surfaces to its sisters as an ambient delta (§env-delta): a `SEND` from `worker://<name>` carrying the loop's deliverable — the `SEND[200]` body, or for an abandonment the reason. A **2xx deliverable is born OPEN** (its body materialized into the parent's packet, not hidden behind a fold): a child's success must reach the parent open and awakening, never a bodyless row. An abandonment (non-2xx) surfaces folded. Every death-path is stamped uniformly, so no termination is silent; collection is the shared world moving, never a verb. **Child orientation.** Beyond the conclusion delta, every turn the packet's status clump surfaces the live things THIS worker *currently holds* — open streams (`## Child Streams`) and unconcluded child workers (`## Active Child Workers`) — as terse `* <status> <path>` pointers (the same shape as the errors section), just above it. A worker is otherwise marked only at spawn and at conclusion; in between it goes silent, so a model loses track of what it holds and premature-terminates. This is ORIENTING STATE, never advice: the model SEES its live subtree (`* 102 worker://worker-x`, `* active sh:///1/2/3`) and reasons for itself — READ/OPEN/KILL via the path — the error stays terse. Empty → omitted, like errors. {§child-orientation} The **pull** side mirrors the push: a path-absent `READ(worker://<name>)` collects the same deliverable on demand — the latest loop's terminal message (the result, or the abandonment reason) for a concluded worker; a worker **still running** has not delivered, so the READ returns **425** (Too Early) and the turn's `SEND[202]` **blocks the loop on the join** ({§join-blocking-collect}) until the worker delivers — the engine holds the join, the model never drives a park. A missing name is 404. So the model never needs to guess a scratch path to "check on" a worker — reading the worker itself yields its outcome or a wait. {§worker-scheme-collect}
|
|
199
199
|
|
|
@@ -203,14 +203,29 @@ Beyond the three creation ops:
|
|
|
203
203
|
|
|
204
204
|
A worker is a **log plus a cancellation scope** — one `AbortController` per worker, reused while live and replaced only once aborted, so a cancel ends the worker as a unit and a later `loop.run` is never born cancelled. A worker's queued loops are advanced by a **drain**: a single per-worker worker that claims loops atomically (status 100→102) and runs each under the worker'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 worker holds open. Cancellation and conclusion are defined against these structures, never wall-clock timing.
|
|
205
205
|
|
|
206
|
+
```mermaid
|
|
207
|
+
stateDiagram-v2
|
|
208
|
+
[*] --> Queued: loop.run
|
|
209
|
+
Queued --> Running: drain claims
|
|
210
|
+
Running --> Parked: wait with live obligations
|
|
211
|
+
Parked --> Queued: obligation settles or arrival
|
|
212
|
+
Running --> Terminal: conclude or fail
|
|
213
|
+
Parked --> Terminal: cancel
|
|
214
|
+
Queued --> Terminal: cancel
|
|
215
|
+
Terminal --> [*]
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
The lifecycle store admits only the guarded transitions shown above: `100 → 102`, `102 → 202`, `202 → 100`, and any unresolved state (`100`, `102`, `202`) to a terminal status. Terminal state is immutable. The queue drain owns claim, the dispatcher owns model-requested park/conclusion, the daemon owns wake/cancellation/restart reconciliation, and the engine owns policy terminals. A racing transition that loses observes the durable winner; it does not overwrite it or report the requested state as fact. {§worker-lifecycle-state-machine}
|
|
219
|
+
|
|
206
220
|
- **One drain advances a worker.** At most one drain is registered for a worker at any instant: a `loop.run` or wake on a worker 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 the worker under one per-worker lock, so the teardown's re-claim cannot race a concurrent start into a double-drain. {§worker-lifecycle-single-drain}
|
|
207
|
-
- **
|
|
221
|
+
- **Cancellation is recursive and reaps every held stream.** `loop.cancel`, worker `KILL`, shutdown, and a worker's `SEND[499]` terminalize every unresolved loop in the cancelled worker subtree and iterate each worker's durable open-subscription rows, invoking each exact callable owner from the process-local live registry. The durable rows answer *what is held*; the live registry answers *how this process tears it down*; the abort signal is a fast-path optimization. There is no implicit detachment. 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 bounded: the executor sends a polite signal then SIGKILL after a consumer-set grace (`PLURNK_SERVICE_EXEC_KILL_GRACE_MS`). A model `KILL[code]` on one live stream instead delivers exactly that signal once (bare `KILL` → the executor's SIGHUP default, `KILL[9]` → SIGKILL). {§worker-lifecycle-total-reap}
|
|
208
222
|
- **A stream's kill binds to the scope it captured at spawn.** A stream captures the worker'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. {§worker-lifecycle-exec-epoch-bound}
|
|
209
223
|
- **A cancelled run is not resurrected by its own torn-down work.** A stream conclusion delivered to a cancelled, idle worker 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 worker. {§worker-lifecycle-no-resurrection}
|
|
210
224
|
- **A stream conclusion always reaches its worker.** When a backgrounded stream concludes, the daemon routes it through the same inject seam as any loop source (§actor-boundary-passive-wake): an active worker folds the conclusion into its next turn; a worker **blocked on a 202 wait** for that stream (§wait-obligation-matrix) **awakens that loop in place** — the blocked loop *is* the continuation, so there is no fresh loop and no summary-as-prompt fiction. The result is never lost: a blocked loop sleeps rather than ending, and the stream's status-transition is the arrival (§actor-boundary-passive-wake) that wakes it; on resume it reads the concluded stream's own state, not a synthetic prompt. {§worker-lifecycle-wake-liveness}
|
|
211
225
|
- **A child worker concluding wakes a parent blocked on it — the topology join.** `worker://` spawn/fork records `parent_worker_id` (§lifecycle-terms). When a worker's drain exits having **concluded** — no `202`-blocked loop, no open stream — the daemon resumes its parent **in place** if the parent is blocked on the join (`#onDrainExit` → the shared `#wakeParkedWorker`, the same 202→100 resume a stream conclusion uses). So a parent that spawns work and blocks (`SEND[202]`) is woken the moment its child finishes; on resume it reads the child's deliverable from the §worker-scheme-collect delta in its own log — a control edge, **never an injected prompt**. The wake recurses upward via the parent's own drain-exit. A child still running — or itself blocked at 202 — is not *concluded*, so it does not wake the parent (it's still a live thing the subtree holds). This is the structured-concurrency join: streams and child workers are the same kind of "live thing a worker holds," driving premature-terminate (§send-premature-terminate), the wake edge, and the collect delta identically. A worker conclusion is a **bounded, un-loseable** wake: if the conclusion fires while the parent is mid-turn (before its block commits), `#wakeParkedWorker` finds it not-yet-slept and records an **owed wake**, which the drain honors when the parent blocks — so a wait awaiting workers **always returns**, never dead-blocks on a conclude-before-block race. (Only a live exec stream, unbounded absent a timeout, may legitimately hold a wait open.) {§worker-lifecycle-child-wake}
|
|
212
|
-
- **An idle run concludes; it does not park.** A loop
|
|
226
|
+
- **An idle run concludes; it does not park.** A loop is idle only when it has neither live obligations nor completed results awaiting their first packet. A live child or stream blocks a `SEND[202]` join; a completed stream, child result, or same-turn retrieval continues directly to the next packet where it is observed. Only after those sets are drained does `SEND[202]` resolve like `SEND[200]`. There is no held-open idle loop and no `loop/quiesced` soft signal. A concluded worker is durable working history and an addressed arrival reawakens it as a new loop. {§worker-lifecycle-idle-is-concluded}
|
|
213
227
|
- **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. {§worker-lifecycle-no-lost-loop}
|
|
228
|
+
- **Restart is owner-loss reconciliation, not replay.** {§worker-lifecycle-restart-recovery} Before opening client transports, the service holds an exclusive database-adjacent daemon lock; a second live owner fails before touching SQLite, while a dead-PID crash claim is replaced atomically without a timeout lease. Boot preserves accepted `100` loops and restores their drains. A `102` loop belonged to a vanished drain/provider call, so it settles `500` with the interruption on its durable row—never replayed across an unknown effect boundary. Every durable-open subscription likewise belonged to a vanished callable: active channels become errored and its row closes `500`. A `202` continuation survives only while a live child obligation remains; after reconciliation, an unblocked park requeues `202→100` and resumes in place. Child terminalization wakes its parked parent on every outcome, including provider exceptions, cancellation, and restart interruption, recursively through the durable parent edges. These operations are idempotent, so an interrupted recovery safely repeats.
|
|
214
229
|
|
|
215
230
|
---
|
|
216
231
|
|
|
@@ -260,9 +275,20 @@ Deferred (#249): grounding the attribution value in real per-turn value flow rat
|
|
|
260
275
|
|
|
261
276
|
Model alias parsing (`parseAliasesFromEnv` / `resolveActiveAlias`) lives in [`@plurnk/plurnk-providers`](https://github.com/plurnk/plurnk-providers). {§provider-instantiation-alias-resolution} Dynamic provider instantiation (`instantiateProvider` / `loadActiveProvider`) lives in `src/core/ProviderInstantiate.ts` here — `import()` resolves package specifiers relative to the calling module, so the dynamic-import path stays in the consumer where the `@plurnk/plurnk-providers-<vendor>` packages are installed.
|
|
262
277
|
|
|
263
|
-
**
|
|
264
|
-
|
|
265
|
-
|
|
278
|
+
**Optional local GBNF is verified at boot.** {§grammar-enforcement-verified-at-boot}
|
|
279
|
+
The ANTLR grammar always defines and validates the PLURNK language. Separately,
|
|
280
|
+
an operator may configure `PLURNK_PROVIDERS_GBNF_<alias>` for a local
|
|
281
|
+
llama-server. The provider must advertise GBNF transport and satisfy a forcing
|
|
282
|
+
probe (`root ::= "PLURNK-RAILS-LIVE"`) or boot fails. The setting is resolved
|
|
283
|
+
per alias and is unset by default. Configuring it on a cloud or endpoint-managed
|
|
284
|
+
provider is an error, not a request for best-effort filtering.
|
|
285
|
+
|
|
286
|
+
**Local constraint truth is independently observed.** {§rail-truth-engine-verdict}
|
|
287
|
+
For a configured local GBNF, the engine independently validates every completed
|
|
288
|
+
emission and stamps `railsAttached: "client"` plus `railsVerdict`. A non-accept
|
|
289
|
+
verdict emits `grammar_unenforced` telemetry if the provider did not already
|
|
290
|
+
report it. With no local GBNF, core adds no rail state and makes no claim about
|
|
291
|
+
endpoint-owned settings; any endpoint telemetry remains provider metadata.
|
|
266
292
|
|
|
267
293
|
```
|
|
268
294
|
PLURNK_MODEL_gemma=openai/macher.gguf
|
|
@@ -287,7 +313,7 @@ Author-facing contract: [plurnk-schemes#1](https://github.com/plurnk/plurnk-sche
|
|
|
287
313
|
Every op targets a URI; the entry key is `(workspace, owner, scheme, pathname)` ({§entry-identity-no-null}). The URI parses per RFC 3986 (`scheme://[authority]/path`). Handler routing and resource identity are separate:
|
|
288
314
|
|
|
289
315
|
- A **registered** scheme is a plurnk namespace: its authority is a leading path segment, folded into the pathname (`Dispatcher.#extractTarget` → `foldAuthorityIntoPath`). So `known://x`, `known:///x`, and pathname `/x` are the same entry — the authority is never a host, and the two-slash and three-slash forms are not distinct resources. {§scheme-address-namespace-fold}
|
|
290
|
-
- A **network resource** keeps its addressed protocol while folding the host into the storage pathname: `https://example.com/page` → `(https, /example.com/page)`. `https` may route through the registered `http` handler, just as `ws` routes through `wss`; that implementation alias never aliases identities. Absolute network URLs are single resources even when their path ends `/` — folder/glob expansion belongs to entry namespaces, never an HTTP origin.
|
|
316
|
+
- A **network resource** keeps its addressed protocol while folding the host into the storage pathname: `https://example.com/page` → `(https, /example.com/page)`. `https` may route through the registered `http` handler, just as `ws` routes through `wss`; that implementation alias never aliases identities. The `SchemeCtx.entries` direct CRUD and standard operation caps both bind to the addressed protocol, regardless of the handler manifest's canonical name. Absolute network URLs are single resources even when their path ends `/` — folder/glob expansion belongs to entry namespaces, never an HTTP origin.
|
|
291
317
|
- The **`file` class is the workspace filesystem** — a mount namespace with its own resolution and naming law, specified below.
|
|
292
318
|
|
|
293
319
|
**The workspace is a mount namespace; `project_root` is the model's `/`.** {§fs-namespace} Chroot semantics: host paths do not exist inside the jail, and no engine surface ever folds a host-absolute spelling onto a member (the convenience fold was run59's biography — #545). The root is **fixed immutably at workspace creation** (headless is forever); the namespace's mount table changes only through the declared membership overlay (§membership), never by re-rooting. At `project_root = /` the jail is the whole filesystem and every rule below degenerates to identity — the design's proof case, and the common benchmark topology.
|
|
@@ -316,9 +342,13 @@ Per author contract (`readEntry` / `writeEntry` / `deleteEntry`). Engine drives
|
|
|
316
342
|
|
|
317
343
|
### §op-methods Op methods
|
|
318
344
|
|
|
319
|
-
Per author contract (`
|
|
345
|
+
Per author contract (`editBatch`/`read`/`open`/`fold`/`find`/`send`/`exec?`). Engine dispatches EDIT resource batches through `editBatch`; every other op dispatches by its lowercased `PlurnkStatement.op`. {§op-methods-op-dispatch} COPY and MOVE are NOT scheme methods — engine orchestrates over CRUD primitives (§copy/§move).
|
|
320
346
|
|
|
321
|
-
- **
|
|
347
|
+
- **A continuing turn executes in MODE phases.** {§op-mode-phases} A model turn describes intended effects and requested observations; it is not an imperative program whose later statements can consume invisible same-turn results. The engine therefore performs four stable phases: **Mutate** (`EDIT`, `COPY`, `MOVE`, `KILL`, `FOLD`), **Observe** (`FIND`, `READ`, `OPEN`), **Do** (all remaining non-terminal actions, including `EXEC`, `WORK`, `FORK`, and directed `SEND`), then **End** (the terminal `SEND`). `PLAN` remains the turn anchor and is recorded before those phases. Authored order is preserved within each phase. A result still lands in the next packet; phasing makes that result describe settled state instead of an accidental intermediate state.
|
|
348
|
+
|
|
349
|
+
- **Decisive operations settle before the next scheduled operation.** {§op-synchronous} The dispatcher `await`s every decisive operation. The only operations that return tracked work still in flight are the operations whose purpose is to create concurrency: `FORK`, `WORK`, and stream-producing `EXEC` handlers. MODE changes scheduling, not completion semantics. This is why a same-turn `KILL + SEND[200]` concludes (§send-premature-terminate): `KILL` synchronously flips the worker's live loops terminal (`engine_terminate_worker_live_loops`) before the End phase judges the pending set, while the physical scope reap rides `cancelWorker` asynchronously and invisibly.
|
|
350
|
+
|
|
351
|
+
- **Same-resource EDITs are one mutation.** {§edit-batch} Every EDIT targeting the same canonical resource and channel in one turn applies to the resource's one pre-turn snapshot. The scheme validates the complete batch before writing, applies disjoint ranges from the highest original coordinate downward, and commits its one resulting revision atomically; reversing the statements cannot change that revision. A failing statement rejects that resource batch without a partial write; independent resource batches remain independent. Whole-resource replacement or creation cannot coexist with another EDIT in the same batch, selected ranges may not overlap, and a boundary insertion may occur at most once at each boundary. Prepend (`<0>`), append (`<-1>`), and fractional insertions name distinct snapshot boundaries and compose with non-overlapping ranges; two insertions naming the same boundary fail rather than acquiring an authored-order meaning. Proposal-gated schemes expose one proposal for the resource batch and accept all or none. The public scheme contract is batch-shaped: a scheme must never emulate this guarantee by applying individual EDITs sequentially.
|
|
322
352
|
|
|
323
353
|
### §orchestration Cross-scheme orchestration
|
|
324
354
|
|
|
@@ -349,7 +379,7 @@ Directed SEND (non-null path) routes to scheme's `send`. Status = intent:
|
|
|
349
379
|
|
|
350
380
|
- **Log speaks the universal query contract** {§log-uniform-query} — `FIND(log://…)` works like every scheme's FIND (the jumbo model ran the canon-documented `FIND(log:///**):#…#` six times into a bare 501 — log was the one scheme outside the universal paradigm, by accident not decision). Candidates are the worker's rows scoped by the coordinate hierarchy ({§log-coordinate-hierarchy}); each is projected EXACTLY as READ shows it (the rx unwrap — FIND must match what the model can retrieve); the shared `Matcher.matchCandidates` ({§find-source-agnostic}) applies any content dialect; results are catalog-shaped items keyed `log:///loop/turn/seq/OP` with `{mimetype, tokens, lines}`, and the fan-out retargets per-row READs so `READ(log://…):matcher` composes identically to entries (#286). A tag signal filters candidates by the model's own region tags ({§log-region-tagging}); log rows carry no symbol channels (`@graph` 501, an honest absence). `~semantic` is an HONEST 501 until log rows receive the same exhaustive vector coverage as entries; storage stays the event stream (the Q3/Option-B decision: uniform QUERY contract, honest distinct storage).
|
|
351
381
|
- **The content matcher is source-agnostic** {§find-source-agnostic} — `Matcher.matchCandidates(body, candidates, mimetypes)` applies a content matcher (regex/jsonpath/xpath/glob) to candidates from ANY source, keyed by the caller's own identity (a pathname for entries, a `loop/turn/seq` coordinate for log). The matcher never cares what table the content came from, so FIND/READ with every content dialect works uniformly across schemes BY CONSTRUCTION — `EntryFind` and `Log.find` run the ONE shared primitive rather than re-implementing per scheme. This is the query-layer half of the log-uniformity decision (Q3, Option B): log stays its own event stream, but its rows are candidates the shared matcher covers like any entry's content.
|
|
352
|
-
- **
|
|
382
|
+
- **Matching carries provenance and navigation coordinates** {§matcher-selection-signal} — a pattern match returns the SOURCE LINE containing it, with its line number (`42:I bought Alice some flowers`, never `1:Alice`), and its FIND item carries both that source footprint and the corresponding rows accepted by scoped READ. `matchSpan` is `{lineStart,lineEnd,rowStart,rowEnd}`; rows equal lines for line-navigable text and identify top-level readable items for structured content. `matchPath` remains the hit's canonical dialect coordinate (for example `$['users'][0]['name']`) when available. A matcher READ writes its internal FIND as a selection-summary row before its span-deduplicated deliveries, so minified or otherwise shared-line hits remain distinguishable without changing the returned source content. The teaching half is grammar's canon (grammar#56).
|
|
353
383
|
|
|
354
384
|
`SEND[410](path[#fragment])` also deletes the target entry/channel — an implemented side-effect, NOT taught to the model and with no live/demo surface. The model-facing delete idiom is KILL (§move).
|
|
355
385
|
|
|
@@ -627,9 +657,10 @@ AST: `{ op: "FIND", target (scope), body: MatcherBody | null (predicate), signal
|
|
|
627
657
|
|
|
628
658
|
- Filters entries within scope. A **bare** path is the exact entry; an explicit **glob** expands to a scope; `#regex#` filters by pathname. A trailing slash is a folder scope only for a scheme whose manifest declares `folderScopes: true`; otherwise it is ordinary resource syntax and READ dispatches it directly. This is an explicit plugin contract, never inferred from URL punctuation: new schemes cannot accidentally turn a root resource into unbounded fan-out. For declared folder schemes the same target contract governs FIND and READ — bare = the entry, folder/glob = a scope (#286). {§find-scope-prefix-filter}
|
|
629
659
|
- `body` matcher operates on entry content (glob/regex/jsonpath/xpath), per grammar plurnk.md §"Body matcher dispatch"; the path-glob lives in the (target), not the body. {§find-glob-filter-on-content}
|
|
660
|
+
- Every matcher operates only over the candidate set selected by `(target)` and `[tags]`; relation matchers do not bypass that selection. Semantic ranking is exhaustive within that candidate set, then applies its result policy—never rank the wider corpus and discard out-of-scope hits afterward, which changes top-K meaning and leaks entries across an exact target. A semantic matcher with no `<scope>` returns the configured `PLURNK_SERVICE_SEMANTIC_TOP_K` highest-ranked results. An integer scope overrides that count; a leading decimal is a minimum cosine-similarity threshold, optionally followed by a result cap. The ordinary FIND render budget remains independent of semantic ranking. {§find-semantic-default-top-k}
|
|
630
661
|
- `signal` is a tag filter; entries match if they have ALL listed tags. {§find-tag-filter-and-semantics}
|
|
631
662
|
- Workspace + scheme scoped — no cross-workspace/cross-scheme leakage. {§find-scoped-isolation}
|
|
632
|
-
- Returns `FindResult { status, content, mimetype, results: MatchItem[], matches, pathnames }`. The matcher sets the unit (#286). A **body-less** FIND is the **catalog**: one item per *entry* — `{ path, seconds?, tags?, channels: { <uri>: { mimetype, tokens, lines } } }` (the addressable path, per-channel `{mimetype, tokens, lines}` keyed by URI — default channel → the bare path, non-default → `path#channel` — plus `tags` and a live `seconds` stream age), the manifest's per-scheme slice. A **matcher** FIND resolves to one item per *match*: the entry's catalog row plus the `matchSpan` `{lineStart,
|
|
663
|
+
- Returns `FindResult { status, content, mimetype, results: MatchItem[], matches, pathnames }`. The matcher sets the unit (#286). A **body-less** FIND is the **catalog**: one item per *entry* — `{ path, seconds?, tags?, channels: { <uri>: { mimetype, tokens, lines } } }` (the addressable path, per-channel `{mimetype, tokens, lines}` keyed by URI — default channel → the bare path, non-default → `path#channel` — plus `tags` and a live `seconds` stream age), the manifest's per-scheme slice. A **matcher** FIND resolves to one item per *match*: the entry's catalog row plus the `matchSpan` `{lineStart,lineEnd,rowStart,rowEnd}` it hit. The line pair records source provenance; the row pair is directly reusable as scoped READ input. **A file with N matches yields N items** — the same row repeated, one span each; there is no `matchLines` array. The unit is uniform across every dialect — glob/regex/jsonpath/xpath select source spans, `~`semantic the ranked chunk's span, `@`graph the matched symbol's span — and the mimetype handler maps each to readable rows. Order is match order (rank for `~`semantic, source order otherwise); a miss contributes nothing; identical source spans dedup. `content` is the items as a JSON array (`application/json`). {§find-result-catalog-rows} **Over the render budget, FIND returns a count, not contents** (#418, `PLURNK_SERVICE_FIND_MAX_MATCHES`): a repo-scale `FIND(**)` over a 19k-entry workspace can't enumerate — materializing every match overflows the window, and a clean grind must not be a crash-and-recover. When the match set exceeds the budget the result carries `overflow: N` and its `content` states the fact (`"N entries match, exceeding the render budget — not enumerated"`), `text/markdown` not the JSON array; its enumerated `results`, `matches`, and `pathnames` arrays are EMPTY so no caller can perform hidden work from content the model was denied, while `overflow` and `itemsTokenTotal` report the full count and aggregate weight. INDEPENDENT of window size — even a 256k window should not render a whole repo's catalog into one turn. `0`/unset = no gate (small workspaces enumerate as before). {§find-count-not-contents}
|
|
633
664
|
|
|
634
665
|
### §send SEND
|
|
635
666
|
|
|
@@ -638,7 +669,7 @@ AST: `{ op: "SEND", target: ParsedPath | null, body: SendBody | null, signal: nu
|
|
|
638
669
|
- **Broadcast** (path null): the loop's disposition verb. `signal` is the model's *claim* about the worker's state — see the terminal contract.
|
|
639
670
|
- **Directed** (path non-null): routes to `scheme.send` per §send-dispatch — stream control / cross-worker irc, never a loop terminal.
|
|
640
671
|
|
|
641
|
-
**Terminal contract —
|
|
672
|
+
**Terminal contract — three signals over a structured-concurrency scope.** A broadcast SEND's status is a claim the engine **verifies against the worker's actual state**, never a verdict it trusts. The model signals one intention — **continue (102)**, **done (200)**, or **wait (202)** — plus **499**, give up. An **obligation** is live work the loop set going and has not consumed: **J**, a child or stream it spawned (the join), and **R**, this turn's own retrievals (READ/FIND/OPEN, whose results land next turn). `<T,P>` is an explicit timeout / poll override for external streaming work; ordinary child joins are event-driven and carry no polling fallback.
|
|
642
673
|
|
|
643
674
|
| intention | ∅ (no obligation) | J (live spawned work) | R (pending retrieval) |
|
|
644
675
|
|---|---|---|---|
|
|
@@ -646,16 +677,16 @@ AST: `{ op: "SEND", target: ParsedPath | null, body: SendBody | null, signal: nu
|
|
|
646
677
|
| **200** done | **resolved** — terminal, loop ends | **refused** — Premature-Terminate (KILL to abandon, or wait) | **refused** — forced next turn to see what you fetched |
|
|
647
678
|
| **202** wait | **resolves like 200** — a wait on zero things is satisfied; `<-1>+∅` is a bid to hang the agent, folded to done, never honored | **block on the join** — the loop sleeps (`<T>`/`<-1>` bound it, `<P>` polls); its work's conclusion **reawakens the same loop**, prompt intact (§worker-lifecycle-child-wake, §worker-lifecycle-wake-liveness) | resolves next turn (≈ continue) |
|
|
648
679
|
|
|
649
|
-
**499** gives up regardless of obligations — the model's one self-decided failure (§state-terms)
|
|
680
|
+
**499** gives up regardless of obligations and cancels the unresolved descendant scope — the model's one self-decided failure (§state-terms). The surface is small on purpose. The **one** non-obvious cell is **200 with an obligation in flight** — a contradiction (you claimed done while you owe work), which the engine holds you to via Premature-Terminate below. A child join is bounded by the child's terminal transition; an external stream may carry an explicit `<T,P>` policy. {§wait-obligation-matrix}
|
|
650
681
|
|
|
651
|
-
**An
|
|
682
|
+
**An externally-authored terminal is marked as state, never re-worded.** `terminated_by` names who ended a loop when the model did not: `'cancel'` for an externally-cancelled loop (§methods-loop-cancel); `NULL` = the model's own terminal, whose status already carries the story. COLLECT renders the named act as a marker before any preserved last words, so cancellation cannot masquerade as a deliverable. An already-drained join is the model's own successful terminal and needs no special marker.
|
|
652
683
|
|
|
653
684
|
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.
|
|
654
685
|
|
|
655
686
|
**Three engine error states verify the claim.** None is a status code the model learns; all 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. (All live at `Engine.runLoop`'s turn close.)
|
|
656
687
|
|
|
657
688
|
- **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 your work is done, conclude with 200. If you're waiting on a child or stream you spawned, SEND[202] to block on it — a 202 with nothing to wait on simply concludes."*
|
|
658
|
-
- **Premature terminate — the pending set** {§send-premature-terminate} — `SEND[200]`
|
|
689
|
+
- **Premature terminate — the pending set** {§send-premature-terminate} — `SEND[200]` is gated by one rule: *nothing pending may be silently discarded*. Pending work has two states: **live obligations** (open streams/spawns and live child workers) and **completed-but-unobserved results** (same-turn READ/FIND/OPEN results, terminal stream output without a terminal foisted READ, and child results queued for the next packet). Completion is not delivery; a result becomes observed only after crossing a packet boundary. The set is judged at the terminal's own dispatch, after earlier operations in the emission. `[200]` over any member is refused 409 and the loop continues; `[499]` deliberately abandons regardless. `SEND[202]` parks only on live obligations. If work has completed but is unobserved, it continues directly to the next packet because the wake edge has already fired; only a genuinely empty set resolves immediately like `[200]`. {§send-undelivered-child-term}
|
|
659
690
|
- **SEND[300] is an operator question — a PROPOSAL, the stop-the-world system file edits and MCP auths ride** (owner ruling, #346). Enablement cascades: `PLURNK_QUESTIONS=0` is a servicewide ceiling; otherwise the client affirmatively requests per workspace (`settings.questions: true` at workspace.create), which ALSO injects the questions.md teaching — capability and teaching gate as one. Enabled: the `;`-delimited body parses leniently (first segment the question, the rest choices; zero choices = an open question — never malformed), and the ask raises a proposal: dispatch stops the world, `loop/proposal` carries `{question, choices}` in attrs, and the client's `loop.resolve {decision:"accept", body}` delivers the ANSWER — written into the ask's own model-facing rx (`{"status":200,"body":…}`), read next packet. Reject/timeout resolve through the standard §proposal semantics; the turn records a continue either way (never a 300 terminal), and the loop simply proceeds. Loop auto never auto-answers a question — it exists precisely to stop the world for a human, and the workspace opted in. Disabled: refused 409 with a self-decide steer, never a park into the void. {§send-300-choices}
|
|
660
691
|
|
|
661
692
|
### §exec EXEC
|
|
@@ -666,7 +697,7 @@ Engine routes unconditionally to `exec` scheme (the `(target)` slot is a local p
|
|
|
666
697
|
|
|
667
698
|
**Runtime fall-through** {§exec-runtime-fallthrough} — an UNREGISTERED runtime tag falls through to the shell with the tag as the command word: `EXEC[go]:test ./...` runs as sh's `go test ./...` (#350, the execs architect's dispatch spec automating the owner's execs#21 ruling: per-tool runtimes — go, cargo, make, npm — never earn tags; sh is the general answer). The fall-through rides sh's OWN policy gate (a workspace that disabled sh gets the refusal, never a side door), its output entry lands under `sh://` (it ran on sh, no phantom scheme), and telemetry records `exec_runtime_fallthrough {requested}` — which tags models reach FOR is the data that would ever justify a real runtime, never guesswork. A typo'd tag becomes the shell's own clear 127. No new surface: anything expressible as `EXEC[foo]:bar` was expressible as `EXEC[sh]:foo bar`. The 501 remains for a fall-through-ineligible dispatch (sh itself unregistered or workspace-disabled).
|
|
668
699
|
|
|
669
|
-
**Timeout and poll — `<T,P>` on the `<L>` slot (grammar 0.74.20).** EXEC repurposes the line-marker slot as `<timeout, poll>` in **seconds** (consistent with the `seconds=` stream-age render). `T` (mark[0]) caps the spawn's lifetime: at `T>0` the service aborts it (a bounded reap — polite signal then SIGKILL after `PLURNK_SERVICE_EXEC_KILL_GRACE_MS`) and stamps the stream **504**, distinct from a deliberate kill (499) or a clean exit (200). `-1` / absent → unbounded (loop-life bounded), the background-stream behavior. **`0` → turn-scoped**: the stream is reaped at the worker's *next pre-turn* (via the registry abort, before the turn's own spawns), so it never survives into the subsequent turn; its terminal output surfaces born-OPEN like any close (§exec-stream). {§exec-timeout} `P` (mark[1]) is the **poll cadence**, stored on the subscription: while the loop is *blocked on a `SEND[202]` wait* for that stream
|
|
700
|
+
**Timeout and poll — `<T,P>` on the `<L>` slot (grammar 0.74.20).** EXEC repurposes the line-marker slot as `<timeout, poll>` in **seconds** (consistent with the `seconds=` stream-age render). `T` (mark[0]) caps the spawn's lifetime: at `T>0` the service aborts it (a bounded reap — polite signal then SIGKILL after `PLURNK_SERVICE_EXEC_KILL_GRACE_MS`) and stamps the stream **504**, distinct from a deliberate kill (499) or a clean exit (200). `-1` / absent → unbounded (loop-life bounded), the background-stream behavior. **`0` → turn-scoped**: the stream is reaped at the worker's *next pre-turn* (via the registry abort, before the turn's own spawns), so it never survives into the subsequent turn; its terminal output surfaces born-OPEN like any close (§exec-stream). {§exec-timeout} `P` (mark[1]) is the **poll cadence**, stored on the subscription: while the loop is *blocked on a `SEND[202]` wait* for that stream, the daemon arms a per-worker timer for the tightest open poll cadence and resumes the blocked loop every P seconds (floored by `PLURNK_SERVICE_EXEC_WAIT_MS` so it cannot tick faster than a turn settles) to inspect progress. It does **nothing while the loop is active** because ambient stream deltas already surface progress. An open stream without `P` uses exponential backoff (`PLURNK_SERVICE_EXEC_POLL_SEC` and `PLURNK_SERVICE_EXEC_POLL_TURNS`); explicit `<,P>` wins and `<,0>` disables polling. Child-only joins never use this timer: child settlement is their durable wake edge. {§exec-poll}
|
|
670
701
|
|
|
671
702
|
**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 worker waits for a human gate, then spawns and writes stdout/stderr to channels of a `<runtime>:///<loop>/<turn>/<seq>` entry (the runtime tag is the URI scheme, §exec/#240; the coordinate matches the op's log-row coordinate, e.g. `sh:///1/1/2`), returning `102 Processing` immediately. Channel state transitions (`active` → `closed`/`errored`) drive what the model sees at subsequent turn boundaries (§channel-state). {§exec-host-proposes}
|
|
672
703
|
|
|
@@ -678,7 +709,7 @@ Engine routes unconditionally to `exec` scheme (the `(target)` slot is a local p
|
|
|
678
709
|
|
|
679
710
|
A `read` runtime (observes external state, e.g. search) or `pure` runtime (no observable effect, e.g. `:memory:` sqlite) is side-effect-free → **auto-run**: no proposal, no human gate, no notification. It skips the gate a host command faces, but it does NOT resolve in-band — like every exec it backgrounds and streams, its output reaching the model through the environment-observation injector (a foisted READ of the stream's new bytes each turn, §exec-stream), never a same-turn receipt. {§exec-readpure-ungated}
|
|
680
711
|
|
|
681
|
-
**Stream surfacing.** An exec's output is *observed, not fetched*. Each turn the environment-observation injector
|
|
712
|
+
**Stream surfacing.** An exec's output is *observed, not fetched*. Each turn the environment-observation injector reads each owned channel from its byte cursor and foists the new bytes as an `origin=plurnk` READ at `<runtime>:///<coord>#<channel>`. Streaming deltas are folded; the terminal delta is born OPEN. Because this is pushed content, the OPEN body obeys `PLURNK_SERVICE_ARRIVAL_PREVIEW_LINES`; a cut names the full stream address for an explicit READ. A stream that closes before a same-turn wait remains pending until this terminal READ crosses the next packet boundary. The EXEC row separately records the command. {§exec-stream}
|
|
682
713
|
|
|
683
714
|
`SEND[499](exec:///<loop>/<turn>/<seq>)` cancels the in-flight subprocess via the subscription registry's stored `AbortController` — the coordinate addresses the spawn (`exec://` is the process-control face); the `<runtime>://` output entry delegates the same KILL to the one `Exec` handler that owns the abort state (§stream-control).
|
|
684
715
|
|
|
@@ -716,9 +747,14 @@ Streams are static content from the engine's perspective — content arrives ove
|
|
|
716
747
|
|
|
717
748
|
### §subscriptions Subscriptions
|
|
718
749
|
|
|
719
|
-
READ on a streaming scheme is a subscription, not a one-shot. Scheme opens the connection (SSE/WS/subprocess), returns `102 Processing` immediately, stays alive.
|
|
750
|
+
READ on a streaming scheme is a subscription, not a one-shot. Scheme opens the connection (SSE/WS/subprocess), returns `102 Processing` immediately, stays alive. The service records durable subscription identity and metadata in SQLite and retains the callable `SubscriptionHandle` only in its process-local live registry. `SEND[499]`, worker cancellation, turn-scoped reap, and shutdown all route through that one live registry; no handler-specific cancellation hook or database access is part of the plugin contract. {§subscriptions-subscription-registry-routes-cancellation}
|
|
751
|
+
|
|
752
|
+
The durable row is lifecycle evidence and the lookup key, not a serialized callback. `subscriptions.open()` establishes both halves before yielding; `subscriptions.close("done" | "error" | "cancelled")` settles channel state and status (200/500/499), closes the durable row, wakes the worker when appropriate, and unregisters the live handle. A durable open row without a live handle is an explicit lifecycle failure, never a fabricated cancellation success. Channel state (§channel-state) + log entries (§no-chunk-rows) carry lifecycle.
|
|
720
753
|
|
|
721
|
-
|
|
754
|
+
At process restart every still-open row is necessarily missing its callable owner. Boot
|
|
755
|
+
settles it as interruption (`500`) and errors active channels before evaluating parked
|
|
756
|
+
loops (§worker-lifecycle-restart-recovery); it never reports cancellation (`499`) or
|
|
757
|
+
pretends to reconstruct an opaque plugin connection.
|
|
722
758
|
|
|
723
759
|
FOLD/OPEN toggles `log_entries.expanded` (§open-fold) — a per-worker render bit, never the subscription registry. FOLDing a streaming entry's log row collapses its body out of the packet but leaves the live stream running: curation is render-only, never cancellation. {§subscriptions-fold-keeps-subscription}
|
|
724
760
|
|
|
@@ -738,7 +774,7 @@ Model sees lifecycle events in the `log` section per turn.
|
|
|
738
774
|
|
|
739
775
|
### §stream-control SEND for stream control
|
|
740
776
|
|
|
741
|
-
- **Cancel:** `<<SEND[499](sse://feed/x)::SEND` —
|
|
777
|
+
- **Cancel:** `<<SEND[499](sse://feed/x)::SEND` — the service invokes the handle registered by `subscriptions.open()` and aborts the composed subscription signal.
|
|
742
778
|
- **Write:** `<<SEND[200](wss://feed/x):body:SEND` — pipes body into active connection (WS, exec stdin, etc.).
|
|
743
779
|
|
|
744
780
|
### §stream-constraints Engine constraints
|
|
@@ -883,7 +919,7 @@ Model selection: separate alias cascade in `ProviderRegistry` (§provider-instan
|
|
|
883
919
|
| `PLURNK_SERVICE_MD_<ALIAS>` | (unset) | Operator reference doc: materializes `<path>` as `plurnk:///<ALIAS>.md`, auto-READ into every model worker's turn 0 (§actor-boundary). `~` expands to home. |
|
|
884
920
|
| `PLURNK_SERVICE_FILES_ITEMS` | `-1` | Turn-0 catalog preview, one `FIND(scheme:///**)` per scheme. Memory/scratch/docs always full; the first-`N` cap applies **only** to the `file` list. `-1` = all full; positive `N` = file list first-N (memory still full); `0` / unset = off (§actor-boundary-catalog-preview). |
|
|
885
921
|
| `PLURNK_SERVICE_PROPOSAL_TIMEOUT_MS` | (empty — waits indefinitely) | ms wait for a proposed entry (status=202) to be resolved before timing out. |
|
|
886
|
-
| `PLURNK_PROVIDERS_REASONING` + `_BUDGET` | `adaptive` / (unset) | The activation/budget split (a numeric budget silently flipping template flags was secret flag-setting). `off | adaptive | on`; budget (tokens) REQUIRED when on.
|
|
922
|
+
| `PLURNK_PROVIDERS_REASONING` + `_BUDGET` | `adaptive` / (unset) | The activation/budget split (a numeric budget silently flipping template flags was secret flag-setting). `off | adaptive | on`; budget (tokens) REQUIRED when on. F7 coupling: llama-server honors only the box's `--reasoning-budget` launch flag (must equal the budget; boot-warned). |
|
|
887
923
|
| `PLURNK_PROVIDERS_FETCH_TIMEOUT` | `600000` | Service-wide ms ceiling on any outbound request (providers, future http schemes). Module-specific overrides are allowed below the ceiling. |
|
|
888
924
|
|
|
889
925
|
Every knob listed is enforced — the engine reads and acts on it; `.env.defaults` is the authoritative default (reader-declares, §operator-config-env-defaults).
|
|
@@ -892,7 +928,7 @@ Every knob listed is enforced — the engine reads and acts on it; `.env.default
|
|
|
892
928
|
- **Ceiling** (most-restrictive-wins) — an operator-set hard bound nothing downstream may exceed: not a lower-precedence file, not a per-workspace constraint, not a per-call RPC arg. `PLURNK_SERVICE_GIT_ALLOWED` (`=0` flatly denies git service-wide, §membership), `PLURNK_SERVICE_MAX_COMMANDS`, `PLURNK_SERVICE_MAX_STRIKES`, `PLURNK_PROVIDERS_FETCH_TIMEOUT` (module overrides allowed only *below* it), and `PLURNK_SERVICE_MAX_TURNS` (`-1` ships it off; a positive value caps the per-call request). The sandbox/cost guarantee: the operator caps it; no client widens it.
|
|
893
929
|
- **Default** (explicit-wins) — a fallback the most-specific setter replaces freely: `PLURNK_MODEL` (a `loop.run({alias})` overrides it), `PLURNK_SERVICE_REQUIREMENTS` (the per-call requirements default), and the config-time vars (`HOST` / `PORT` / `DB_PATH`).
|
|
894
930
|
|
|
895
|
-
**The shipped `.env.defaults` is itself under test** (no active `PLURNK_SERVICE_MD_*` doc alias — the policy is a SECTION, a doc default double-injects it; no active `PLURNK_MODEL`;
|
|
931
|
+
**The shipped `.env.defaults` is itself under test** (no active `PLURNK_SERVICE_MD_*` doc alias — the policy is a SECTION, a doc default double-injects it; no active `PLURNK_MODEL`; no active local GBNF constraint; the policy renders in exactly one packet section): every other tier runs the test cascade, so shipped-default regressions are invisible to it by construction. {§operator-config-shipped-defaults} Its companion **flag-parity** check binds code and template both ways: every `PLURNK_SERVICE_*` the service reads has a `.env.defaults` line (a floor, a `--flag`, a legend entry) and every declared `PLURNK_SERVICE_*` is read — so a half-landed rename (a missed file, a script-glob gap) fails a test instead of a user's boot, and a dead knob can't ship. {§operator-config-flag-parity}
|
|
896
932
|
|
|
897
933
|
Enforcement is per-use-site — no central most-restrictive pass; each ceiling is checked where it bites. `PLURNK_SERVICE_MAX_TURNS` ships **off** (`-1` = no cap; the loop ends via SEND, budget, strikes, or cycle detection) and, when an operator sets a positive value, the per-call request is `min()`-capped against it. {§operator-config-max-turns-ceiling}
|
|
898
934
|
|
|
@@ -1006,7 +1042,7 @@ registry.registerMethod("loop.run", {
|
|
|
1006
1042
|
| `loop.run` | `prompt: string`, `maxTurns?: number`, `alias?: string`, `model?: 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 may block on a `SEND[202]` wait for its own spawned work (§worker-lifecycle-wake-liveness). The loop's outcome — `finalStatus`, `turnIds`, `hitMaxTurns`, `usage` — arrives on the **`loop/terminated`** event. **Per-loop model selection** (#414/#598): optional `model` (client-resolved `<provider>/<model>`, wins) or `alias` (a declared `PLURNK_MODEL_<alias>`) overrides the boot-time `PLURNK_MODEL` for a newly created loop. The fully resolved `{ alias, provider, model, baseUrl? }` is persisted on that loop and is immutable through every turn, queue claim, async wake, and daemon restart; no resume path may substitute the boot default. A prompt folded into an active or parked loop must resolve to the same selection; a conflicting selection fails loudly and requires the caller to conclude or cancel before opening a loop on another model. Provider instances are cached, an unresolvable spec fails before enqueue, and neither selector runs the boot default. Mid-loop switching is not an implicit side effect of prompt injection; a future such capability requires its own explicit atomic operation. Optional `flags` carries per-loop flags (`{auto?: boolean}`; see §engine-rails). Streams `log/entry` and `loop/proposal` during. `longRunning: false`. {§methods-loop-run} {§methods-loop-run-model} |
|
|
1007
1043
|
| `proposal.list` | — | `{ proposals: [{logEntryId, workerId, loopId, turnId, op, suffix, target, body, attrs, flags, at}] }` | Every pending (`state='proposed'`) stop-the-world proposal in the attached workspace — the indefinite-wait ruling's mandatory companion: `loop/proposal` is a notification, so a client reconnecting during a stopped world (possibly days old) DISCOVERS it here and answers via the ordinary `loop.resolve`. `attrs` carries `{question, choices}` for [300] asks. {§proposal-list} |
|
|
1008
1044
|
| `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. |
|
|
1009
|
-
| `loop.cancel` | `reason?: string` | `{ cancelled, workerId, reason }` |
|
|
1045
|
+
| `loop.cancel` | `reason?: string` | `{ cancelled, workerId, reason }` | Cancel the attached worker's structured scope. Every unresolved loop (`100`, `102`, or `202`) in the worker subtree closes at 499 and every held subscription is reaped; descendants never detach implicitly. Default reason `user_cancelled`. `cancelled` reports whether the attached worker had process-local work when the request arrived; durable parked or queued work is still terminalized when it is `false`. Cancellation is provenanced: each affected row records `terminated_by='cancel'` and the reason, and each terminal event carries the same message. {§methods-loop-cancel} |
|
|
1010
1046
|
| `providers.list` | none | `{ aliases: ProviderAlias[] }` | Lists configured `PLURNK_MODEL_<alias>` entries with `{alias, provider, model, active}`. Clients use to populate model-selection UI. |
|
|
1011
1047
|
|
|
1012
1048
|
**Reads**
|
|
@@ -1022,7 +1058,10 @@ registry.registerMethod("loop.run", {
|
|
|
1022
1058
|
|
|
1023
1059
|
Per the **Speak in DSL, not plumbing** rule (AGENTS.md): `op.*` methods construct DSL statements internally and dispatch through `Engine.dispatch`. {§methods-op-mirror} Param shapes are ergonomic (semantic names, not HEREDOC slots); semantics are the DSL's.
|
|
1024
1060
|
|
|
1025
|
-
Each
|
|
1061
|
+
Each client action creates one terminal journal segment (§connection-lifecycle). Every
|
|
1062
|
+
statement in that action creates an ordered turn inside the segment, dispatches, fires
|
|
1063
|
+
`log/entry`, and returns its result. `op.parse` therefore does not manufacture one loop
|
|
1064
|
+
per parsed statement.
|
|
1026
1065
|
|
|
1027
1066
|
Naming: `target` = URI the op acts on; `scope` for FIND; `source`/`destination` for COPY/MOVE; `recipient` for SEND (or null = broadcast); `cwd` for EXEC. `path` is reserved for *identity* — never an RPC operand.
|
|
1028
1067
|
|
|
@@ -1052,7 +1091,7 @@ Future: `subscription.list`, `subscription.cancel` (the latter is `op.send({stat
|
|
|
1052
1091
|
| Notification | Params | When fired |
|
|
1053
1092
|
|--------------------|-------------------------------------|------------|
|
|
1054
1093
|
| `log/entry` | `{ entry: LogEntry }` | Every `log_entries` write. {§notifications-log-entry-notify} |
|
|
1055
|
-
| `loop/terminated` | `{ loopId, finalStatus, hitMaxTurns }` |
|
|
1094
|
+
| `loop/terminated` | `{ workerId, loopId, finalStatus, hitMaxTurns, turnIds, usage, message? }` | One loop reaches terminal status. `workerId` and `loopId` are an inseparable owning coordinate; transports bind completion to the exact loop they started or resumed and never infer either half from workspace traffic. |
|
|
1056
1095
|
| `loop/proposal` | `{ logEntryId, workspaceId, workerId, loopId, turnId, op, target, body, attrs, flags }` | Dispatch pauses on status=202. Carries `flags` so a client can suppress review UI when loop-owned policy will resolve it. Client responds with `loop.resolve` when authority crosses the boundary (or `PLURNK_SERVICE_PROPOSAL_TIMEOUT_MS` fires). |
|
|
1057
1096
|
| `workspace/created` | `{ id, name, projectRoot }` | Any client creates a workspace. |
|
|
1058
1097
|
| `stream/event` | `{ entryId, channel, state, contentLength }` | Channel content grows or state transitions. {§notifications-stream-event-on-channel-change} |
|
|
@@ -1063,7 +1102,16 @@ Future: `subscription.list`, `subscription.cancel` (the latter is `op.send({stat
|
|
|
1063
1102
|
|
|
1064
1103
|
### §connection-lifecycle Client context
|
|
1065
1104
|
|
|
1066
|
-
**
|
|
1105
|
+
**Client action evidence.** A module client is an actor (§machine-processes); its
|
|
1106
|
+
side-effecting `op.*` actions write to its **own worker** with `origin = "client"`.
|
|
1107
|
+
One action owns one journal segment, and all statements parsed from that action are
|
|
1108
|
+
ordered turns inside it. A proposal may hold the segment open while the action crosses
|
|
1109
|
+
an AG-UI interrupt/resume boundary; resolution or failure closes it. These segments
|
|
1110
|
+
record durable action evidence but do not define client-visible lifecycle—AG-UI runs do.
|
|
1111
|
+
Observational actions create no turns or rows. `LOOK` retains a closed, rowless
|
|
1112
|
+
observation segment because the public scheme context and relative `log:///` addressing
|
|
1113
|
+
require an honest numeric loop coordinate; it never leaves active lifecycle behind.
|
|
1114
|
+
Multiple client actors have distinct workers.
|
|
1067
1115
|
|
|
1068
1116
|
`loop.run` and `inject` target the **model's worker** — a separate worker holding the conversation, `origin = "model"`. Both workers share the workspace's one filesystem (§machine-processes); the packet renders only the model's worker, so the client's ops are structurally absent from it — no origin filter (§actor-boundary-isolation). The model worker (`Envelope.ensureModelWorker`) and the client context's worker are distinct, each lazily allocated on first use.
|
|
1069
1117
|
|
|
@@ -1140,7 +1188,7 @@ Search prefetch and direct HTTP READ materialize the same resource contract: pro
|
|
|
1140
1188
|
|
|
1141
1189
|
Coverage protects the composition at distinct seams: HTTP unit tests pin fragmentless-body publication and explicit auxiliary selection; integration tests pin search→materialize→FIND/matcher READ and persistence/publication separation; deterministic model demos require a model-originated fragmentless HTTPS READ whose returned body contains facts absent from search snippets; a live positive-control demo requires a model-originated HTTPS READ and substantive answer from a real sanitized page. Live discovery demos remain diagnostic and may expose model judgment failures without weakening these assertions. **The search gates** (#406, owner ruling) are rail-family accounting — in-memory per-loop state cleaned at the same seam as strikes, restart-drop accepted (a post-restart duplicate re-fetches; the TTL makes it cheap): an IDENTICAL duplicate (same runtime + command in one loop) **strikes and serves** — status 409 (the strike rail counts the turn failure) carrying the prior ranked digest re-read live from the original exec entry, no re-fetch, no provenance prose; the per-turn CAP (`PLURNK_SERVICE_SEARCH_MAX_PER_TURN`) is flood control — 429 with a legible steer, nothing served. {§search-gate}
|
|
1142
1190
|
|
|
1143
|
-
**git is the substrate.** {§membership-git-membership} git-tracked files (`git ls-files` semantics) are members with no explicit overlay — channel-less markers, disk is truth. git absent → no fs-walk (non-git/headless get no substrate membership); `pick` is then the sole source. **Every core git read is in-process by default** {§git-portable-default}: isomorphic-git (exact-pinned), an explicit-`dir` library read — no subprocess, no ambient `GIT_*`, no global/system config — so the read is hermetic
|
|
1191
|
+
**git is the substrate.** {§membership-git-membership} git-tracked files (`git ls-files` semantics) are members with no explicit overlay — channel-less markers, disk is truth. git absent → no fs-walk (non-git/headless get no substrate membership); `pick` is then the sole source. **Every core git read is in-process by default** {§git-portable-default}: isomorphic-git (exact-pinned), an explicit-`dir` library read — no subprocess, no ambient `GIT_*`, no global/system config — so the read is hermetic by construction and works on a sandboxed or git-less host. Covered repository shapes include ordinary and linked worktrees, submodule boundaries, and the differential gitignore corpus. When isomorphic-git cannot read an actual repository shape, membership and status fail with one core-owned error that preserves the upstream cause and names `PLURNK_SERVICE_GIT_NATIVE=1`; an explicit backend change is reproducible, while an automatic retry would depend on whether a system binary happens to exist. The untracked scan is a pruning ignore-walk, **differential-gated**: it must reproduce native `ls-files --others --exclude-standard` across a gitignore edge-case corpus (negations, anchors, nested `.gitignore` precedence, `.git/info/exclude`, dir-vs-glob patterns, embedded-repo boundaries) or it does not ship. `PLURNK_SERVICE_GIT_NATIVE=1` routes membership + status to the system git binary {§git-native-flag} — in-process membership measures ~8× native (~130ms at 20k files) and the status read ~55× (statusMatrix hashes the workdir), so a large or unsupported repo with git installed can select the native backend. **That native arm scrubs ambient `GIT_*`** (#401): a process launched from a git hook inherits `GIT_DIR` — absolute in a worktree checkout — which retargets every child git at the enclosing repo regardless of cwd; project git state binds to the workspace's `project_root`, never to whoever spawned the daemon. One boundary (`git-env.hermeticGitEnv`), threaded through the native arm and every shell-git test fixture. {§membership-git-hermetic}
|
|
1144
1192
|
|
|
1145
1193
|
**Membership is a declared forest of repos.** {§membership-forest} A workspace is not one git repo but a **forest**: membership is the union, over a workspace-declared set of repos, of each repo's `ls-files` (gitlinks/mode-160000 filtered), each path-prefixed by the repo's path relative to `project_root`. The root need not itself be a repo — a non-git parent of ninety repos resolves to all ninety. A worktree, a submodule, a buried repo are not special cases: each is just another declared repo, resolved `rev-parse --show-toplevel` → `ls-files` in the tree it points at.
|
|
1146
1194
|
- **Membership-gated edits.** {§membership-edit-membership-gate} EDIT is bounded by membership exactly as READ is. An existing **member**'s baseline is its entry snapshot — the body channel the model READ, not a fresh disk read — so the diff is naive against the view the model saw, never empty (the write-side CAS, §membership-edit-write-cas, prevents the silent overwrite of out-of-band drift). An existing **non-member** is refused (403) *before* any read or write: the model never reads a file it can't see (no leak into the proposal) and never overwrites one (no wiping a gitignored `.env` it never added). A **new path** stays open — proposal→accept adds it to the manifest. Reaching past membership is `EXEC[sh]`'s job, not the file scheme's.
|
|
@@ -1188,7 +1236,7 @@ The CAS is the **hard backstop**, at the moment of writing, on every accept path
|
|
|
1188
1236
|
- **One rule, every turn: fold the NEWEST memories — never history.** THE DOCTRINE, the project's animating narrative: the log is the model's memory and the model ALONE curates it (FOLD/KILL). The grinder never reaches back into history — it only blocks NEW memories from landing when there is no room, forcing the model to do its own housekeeping. On overflow it folds, in one set-op, the still-open rows of the **newest turn boundary**: the immediately-prior turn's emissions and the current turn's pre-model rows (foists, wake surfaces — every current-turn row at grind time is engine-written). Turn 1 is the **same rule**, not a case: no prior turn exists, so its own foists are the newest material. Folded, not deleted: rows and bodies persist and are re-OPENable, so log *history* is preserved while the render collapses to coordinates. {§grinder-layer1-rollback}
|
|
1189
1237
|
- **Errors, the prompt, AND the plan are exempt.** The grinder never folds an `op='error'` row (the budget-overflow it just minted, a parse failure, an action failure) — errors are the model's durable, curatable record of what went wrong; folding them the moment they matter would blind it to a recurring failure. Nor does it fold the **user prompt** (`plurnk:///prompt/…`, #382): the task the engine foisted is FRAME, not the model's curatable memory — the engine never reclaims the definition of the task it set. Nor a **PLAN row** (#465, owner ruling): the checklist is the model's orientation surface at exactly the moment the grinder fires — folding the plan alongside the work it plans would collapse the reasoning thread a recovery turn steers by, and plans are concise by rule, so exempting them reclaims almost nothing. All three stay OPEN until the model itself FOLDs or KILLs them. {§grinder-errors-exempt}
|
|
1190
1238
|
- **The hard overflow is a RECOVERY TURN first** {§grinder-hard-413-recovery} — (Q4, owner ruling: recoverable strike, NO margin). The overflow lives in foldable HISTORY the model owns, and the grinder never touches history (§grinder-layer1-rollback) — so the first hard overflow, when the packet is over the POLICY ceiling but still within PHYSICS (the provider's real window minus the decode reserve), is sent ANYWAY, once: a minted `budget_overflow` steer states the fact, the remedy (KILL/FOLD history; the budget table lists the heaviest), and the consequence — and the turn counts a strike (`budgetStruck`). The model curates → the next fitting turn clears the grant (a later overflow earns a fresh recovery; chronic overflow still strikes out via the rail). A recovery turn that CONCLUDES is a legitimate 200 — finishing is a way to stop overflowing. **No 95% margin**: the pointer fires at 100% of budget — a margin masks the signal, and the decode is separately reserved.
|
|
1191
|
-
- **Hard stop.** A physically-unsendable packet (over the provider's real window too — physics doesn't negotiate), or a SECOND consecutive hard overflow after the recovery turn (the model was told and declined), abandons the loop at **413 Content Too Large
|
|
1239
|
+
- **Hard stop.** A physically-unsendable packet (over the provider's real window too — physics doesn't negotiate), or a SECOND consecutive hard overflow after the recovery turn (the model was told and declined), abandons the loop at **413 Content Too Large**. Its sibling engine-imposed terminals are HTTP-precise too: `maxTurns` → 429, a strike-out → 500 (508 when cycle-driven) — no longer the old catch-all 499. No further passes. {§grinder-hard-413-abort}
|
|
1192
1240
|
|
|
1193
1241
|
- **Output truncation states its cause** {§tokenomics-output-truncated} — a `finish=length` turn was guillotined at the decode pool (`REASONING + COMPLETION`, one undifferentiated `max_tokens` — §tokenomics-window-partition), and its parse errors are truncation ARTIFACTS: without the cause stated, the model reads "unclosed block" and fixes syntax forever instead of emitting less (run29: unclamped cloud reasoning + a 9-edit emission vs a 12288 pool; the binding cap was plurnk's own — `completion 12281`). One terse `output_truncated` row (413) leads the artifact rows it explains, naming the cap and distinguishing its two shapes so the model reads the right cause: content cut **mid-op** (the severed frame — see {§broken-packet-no-dispatch}) vs the pool consumed with **nothing emitted** (reasoning ran away — the parse `must begin with PLAN` is an artifact of the empty emission, not a malformed turn). The row states the fact, not a remedy.
|
|
1194
1242
|
|
|
@@ -1224,15 +1272,15 @@ The CAS is the **hard backstop**, at the moment of writing, on every accept path
|
|
|
1224
1272
|
|
|
1225
1273
|
**Migration path.** Built. The per-worker world-snapshot the architecture forbade (§machine-processes) is **deleted**; its `[§machine-processes-worker-is-its-log]` conformance test is now green. The pull + the `plurnk`-run fs narration replace it.
|
|
1226
1274
|
|
|
1227
|
-
### §edit-result-render EDIT log rows render
|
|
1275
|
+
### §edit-result-render EDIT log rows render a bounded effect receipt
|
|
1228
1276
|
|
|
1229
1277
|
**Question.** An EDIT's log row exists so the model has a record of what it did. Re-emitting the model's *input* statement (the tx heredoc) records the *intent* but not the *outcome* — the model still has to READ the entry back to confirm "did it land, what does it look like now." And a system delta-EDIT (§env-delta) has no input statement at all. What should an EDIT row's body be?
|
|
1230
1278
|
|
|
1231
|
-
**Decision —
|
|
1279
|
+
**Decision — effect, revision, and bounded join context.** A model-authored EDIT row renders compact metadata (`rev`, `extent`, `change`, and `range`) plus bounded numbered context around that edit's resulting join. The durable result retains the full SHA-256 revision; `rev` abbreviates it to `PLURNK_SERVICE_EDIT_RECEIPT_REVISION_CHARS` for display correlation only and is never an identity, lookup key, or compare-and-swap token. Every row in one resource batch carries the same revision and extent but its own requested marker, normalized source/result ranges, removed/inserted counts, and context. The receipt proves what landed without copying an arbitrarily large changed region into the next packet. A deliberate READ in the same turn is scheduled after mutation (§op-mode-phases) and remains the universal way to request arbitrary current content.
|
|
1232
1280
|
|
|
1233
|
-
**Scope.** The
|
|
1281
|
+
**Scope.** The receipt is computed from the one pre-turn snapshot and committed result and stored structurally on the EDIT's `rx`; reviewer-modified proposals recompute it from the content that actually lands. Text resources report lines; structural JSON resources report top-level items. `PLURNK_SERVICE_EDIT_RECEIPT_CONTEXT_LINES` bounds neighboring lines/items independently for each row. Environment-delta EDITs remain factual state-diff events and carry their resulting span (§env-delta); COPY/MOVE likewise retain their resulting span.
|
|
1234
1282
|
|
|
1235
|
-
**Migration path.**
|
|
1283
|
+
**Migration path.** Built with MODE scheduling and atomic resource batches; the former unbounded resulting-span confirmation is removed from model-authored EDITs.
|
|
1236
1284
|
|
|
1237
1285
|
### §proposal-ownership Loop auto and client YOLO
|
|
1238
1286
|
|
|
@@ -1475,7 +1523,7 @@ Same rule applies across Known, Unknown, Skill, Plurnk, File. Effective mimetype
|
|
|
1475
1523
|
|
|
1476
1524
|
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}
|
|
1477
1525
|
|
|
1478
|
-
An `EDIT` log row renders its **
|
|
1526
|
+
An `EDIT` log row renders its bounded **effect receipt** (`rx.receipt`) as row metadata and join context, not its input statement. Proposal-gated file EDITs compute the accepted receipt from what actually lands. Environment-delta EDITs and COPY/MOVE rows render their resulting `rx.span`. {§edit-result-render}
|
|
1479
1527
|
|
|
1480
1528
|
The `N:` prefix is presentation/reference per plurnk.md ("not part of the source"); stripped before any matcher operation on the log entry.
|
|
1481
1529
|
|
|
@@ -1500,7 +1548,7 @@ Carried from the contract walk; durable.
|
|
|
1500
1548
|
- **READ rx** prefixes each line with `N:` per §render-rule. `sliceLinesRaw` (used by COPY) returns the lines without prefix.
|
|
1501
1549
|
- **FIND body matcher** applies to entry content (all dialects), per-candidate via the in-tree `Matcher.matchAgainstContent` (§matcher-dispatch; status 200 = content hit → entry selected). Scope + tags select candidates in SQL; the path-glob is the (target).
|
|
1502
1550
|
- **OPEN/FOLD** operate on the **log** (`log:///`), not entries (§open-fold) — FOLD collapses a log row to its path, OPEN restores its body. Aimed at an entry scheme they return 501.
|
|
1503
|
-
- **SEND[410]** deletes as a side-effect (not the model idiom; §move): with `#fragment`, that channel only; without, the whole entry. **SEND[499]**
|
|
1551
|
+
- **SEND[410]** deletes as a side-effect (not the model idiom; §move): with `#fragment`, that channel only; without, the whole entry. **SEND[499]** resolves the durable open-subscription row and invokes that subscription's exact callable owner through the process-local live registry (§subscriptions).
|
|
1504
1552
|
- **File scheme** reads disk content with mimetype detected via `Mimetypes.detect({ path })` (plumbed through `PlurnkSchemeContext.mimetypes`). Binary mimetypes → 415 on READ and EDIT.
|
|
1505
1553
|
|
|
1506
1554
|
### §send-status-policy Directed-SEND status code policy
|
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"package":"@plurnk/plurnk-service","version":"1.3.
|
|
1
|
+
{"package":"@plurnk/plurnk-service","version":"1.3.3","revision":"3352dea7a51575d4a1e7804428397e4d649f5c57","dirty":false}
|