@plurnk/plurnk-service 1.0.4 → 1.0.6
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 +55 -31
- package/INSTALL.md +3 -3
- package/README.md +4 -4
- package/SPEC.md +205 -193
- package/dist/Paths.js +1 -1
- package/dist/Paths.js.map +1 -1
- package/dist/core/ChannelWrite.d.ts +18 -18
- package/dist/core/ChannelWrite.d.ts.map +1 -1
- package/dist/core/ChannelWrite.js +17 -17
- package/dist/core/ChannelWrite.js.map +1 -1
- package/dist/core/ChannelWrite.sql +11 -11
- package/dist/core/Dispatcher.d.ts +11 -16
- package/dist/core/Dispatcher.d.ts.map +1 -1
- package/dist/core/Dispatcher.js +122 -125
- package/dist/core/Dispatcher.js.map +1 -1
- package/dist/core/Engine.d.ts +20 -23
- package/dist/core/Engine.d.ts.map +1 -1
- package/dist/core/Engine.js +213 -210
- package/dist/core/Engine.js.map +1 -1
- package/dist/core/Engine.sql +108 -130
- package/dist/core/PacketBuilder.d.ts +10 -11
- package/dist/core/PacketBuilder.d.ts.map +1 -1
- package/dist/core/PacketBuilder.js +204 -128
- package/dist/core/PacketBuilder.js.map +1 -1
- package/dist/core/ProposalLifecycle.d.ts +8 -8
- package/dist/core/ProposalLifecycle.d.ts.map +1 -1
- package/dist/core/ProposalLifecycle.js +10 -10
- package/dist/core/ProposalLifecycle.js.map +1 -1
- package/dist/core/ProviderInstantiate.d.ts +1 -0
- package/dist/core/ProviderInstantiate.d.ts.map +1 -1
- package/dist/core/ProviderInstantiate.js +9 -3
- package/dist/core/ProviderInstantiate.js.map +1 -1
- package/dist/core/SchemeRegistry.d.ts +4 -1
- package/dist/core/SchemeRegistry.d.ts.map +1 -1
- package/dist/core/SchemeRegistry.js +18 -9
- package/dist/core/SchemeRegistry.js.map +1 -1
- package/dist/core/TelemetryChannel.d.ts +5 -5
- package/dist/core/TelemetryChannel.d.ts.map +1 -1
- package/dist/core/TelemetryChannel.js +9 -9
- package/dist/core/TelemetryChannel.js.map +1 -1
- package/dist/core/caps/CapsResolve.js +2 -2
- package/dist/core/caps/CapsResolve.js.map +1 -1
- package/dist/core/caps/DbNotifyCaps.js +2 -2
- package/dist/core/caps/DbNotifyCaps.js.map +1 -1
- package/dist/core/caps/DbSubscriptionCaps.js +7 -7
- package/dist/core/caps/DbSubscriptionCaps.js.map +1 -1
- package/dist/core/caps/SchemeCtxImpl.d.ts +2 -2
- package/dist/core/caps/SchemeCtxImpl.d.ts.map +1 -1
- package/dist/core/caps/SchemeCtxImpl.js +4 -4
- package/dist/core/caps/SchemeCtxImpl.js.map +1 -1
- package/dist/core/content-hash.d.ts +2 -0
- package/dist/core/content-hash.d.ts.map +1 -0
- package/dist/core/content-hash.js +6 -0
- package/dist/core/content-hash.js.map +1 -0
- package/dist/core/fork.d.ts +1 -1
- package/dist/core/fork.d.ts.map +1 -1
- package/dist/core/fork.js +31 -26
- package/dist/core/fork.js.map +1 -1
- package/dist/core/fork.sql +41 -32
- package/dist/core/git-env.d.ts.map +1 -1
- package/dist/core/git-env.js +23 -7
- package/dist/core/git-env.js.map +1 -1
- package/dist/core/git-iso.d.ts +9 -0
- package/dist/core/git-iso.d.ts.map +1 -0
- package/dist/core/git-iso.js +227 -0
- package/dist/core/git-iso.js.map +1 -0
- package/dist/core/git-membership.d.ts +2 -2
- package/dist/core/git-membership.d.ts.map +1 -1
- package/dist/core/git-membership.js +70 -50
- package/dist/core/git-membership.js.map +1 -1
- package/dist/core/git-state.d.ts +1 -1
- package/dist/core/git-state.d.ts.map +1 -1
- package/dist/core/git-state.js +21 -11
- 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 +81 -22
- package/dist/core/packet-wire.js.map +1 -1
- package/dist/core/plurnk-uri.d.ts +2 -2
- package/dist/core/plurnk-uri.d.ts.map +1 -1
- package/dist/core/plurnk-uri.js +19 -12
- package/dist/core/plurnk-uri.js.map +1 -1
- package/dist/core/run-ops.sql +26 -26
- package/dist/core/scheme-types.d.ts +6 -11
- package/dist/core/scheme-types.d.ts.map +1 -1
- package/dist/core/search-gate.js +0 -0
- package/dist/core/search-gate.js.map +1 -1
- package/dist/core/token-ruler.d.ts +2 -0
- package/dist/core/token-ruler.d.ts.map +1 -0
- package/dist/core/token-ruler.js +18 -0
- package/dist/core/token-ruler.js.map +1 -0
- package/dist/core/worker-cap.d.ts +8 -0
- package/dist/core/worker-cap.d.ts.map +1 -0
- package/dist/core/worker-cap.js +20 -0
- package/dist/core/worker-cap.js.map +1 -0
- package/dist/core/{session-settings.d.ts → workspace-settings.d.ts} +5 -5
- package/dist/core/workspace-settings.d.ts.map +1 -0
- package/dist/core/{session-settings.js → workspace-settings.js} +13 -13
- package/dist/core/workspace-settings.js.map +1 -0
- package/dist/digest/Digest.d.ts +3 -3
- package/dist/digest/Digest.d.ts.map +1 -1
- package/dist/digest/Digest.js +79 -73
- package/dist/digest/Digest.js.map +1 -1
- package/dist/digest/digest.sql +18 -18
- package/dist/schemes/Exec.d.ts +9 -2
- package/dist/schemes/Exec.d.ts.map +1 -1
- package/dist/schemes/Exec.js +121 -68
- package/dist/schemes/Exec.js.map +1 -1
- package/dist/schemes/ExecOutputScheme.js +3 -3
- package/dist/schemes/ExecOutputScheme.js.map +1 -1
- package/dist/schemes/File.js +23 -23
- package/dist/schemes/File.js.map +1 -1
- package/dist/schemes/Known.js +5 -5
- package/dist/schemes/Known.js.map +1 -1
- package/dist/schemes/Log.d.ts.map +1 -1
- package/dist/schemes/Log.js +63 -14
- package/dist/schemes/Log.js.map +1 -1
- package/dist/schemes/Log.sql +49 -8
- package/dist/schemes/Plurnk.js +5 -5
- package/dist/schemes/Plurnk.js.map +1 -1
- package/dist/schemes/Skill.js +5 -5
- package/dist/schemes/Skill.js.map +1 -1
- package/dist/schemes/Unknown.js +5 -5
- package/dist/schemes/Unknown.js.map +1 -1
- package/dist/schemes/{Run.d.ts → Worker.d.ts} +2 -2
- package/dist/schemes/Worker.d.ts.map +1 -0
- package/dist/schemes/{Run.js → Worker.js} +67 -67
- package/dist/schemes/Worker.js.map +1 -0
- package/dist/schemes/_entry-crud.js +11 -11
- package/dist/schemes/_entry-crud.js.map +1 -1
- package/dist/schemes/_entry-crud.sql +29 -29
- package/dist/schemes/_entry-find.d.ts +2 -1
- package/dist/schemes/_entry-find.d.ts.map +1 -1
- package/dist/schemes/_entry-find.js +31 -19
- package/dist/schemes/_entry-find.js.map +1 -1
- package/dist/schemes/_entry-find.sql +9 -9
- package/dist/schemes/_entry-graph.d.ts +2 -2
- package/dist/schemes/_entry-graph.d.ts.map +1 -1
- package/dist/schemes/_entry-graph.js +18 -18
- package/dist/schemes/_entry-graph.js.map +1 -1
- package/dist/schemes/_entry-graph.sql +8 -8
- package/dist/schemes/_entry-manifest.d.ts +1 -1
- package/dist/schemes/_entry-manifest.d.ts.map +1 -1
- package/dist/schemes/_entry-manifest.js +74 -50
- package/dist/schemes/_entry-manifest.js.map +1 -1
- package/dist/schemes/_entry-ops.d.ts +4 -4
- package/dist/schemes/_entry-ops.d.ts.map +1 -1
- package/dist/schemes/_entry-ops.js +25 -25
- package/dist/schemes/_entry-ops.js.map +1 -1
- package/dist/schemes/_entry-ops.sql +9 -9
- 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 +23 -4
- package/dist/schemes/_entry-semantic.js.map +1 -1
- package/dist/schemes/_entry-semantic.sql +18 -4
- package/dist/schemes/_entry-send.d.ts +1 -1
- package/dist/schemes/_entry-send.d.ts.map +1 -1
- package/dist/schemes/_entry-send.js +6 -6
- package/dist/schemes/_entry-send.js.map +1 -1
- package/dist/schemes/exec-abort.js +1 -1
- package/dist/schemes/exec-abort.js.map +1 -1
- package/dist/server/Daemon.d.ts +50 -48
- package/dist/server/Daemon.d.ts.map +1 -1
- package/dist/server/Daemon.js +307 -297
- package/dist/server/Daemon.js.map +1 -1
- package/dist/server/client-input.js +20 -20
- package/dist/server/client-input.js.map +1 -1
- package/dist/server/dispatch-as-plurnk.d.ts +1 -1
- package/dist/server/dispatch-as-plurnk.d.ts.map +1 -1
- package/dist/server/dispatch-as-plurnk.js +7 -7
- package/dist/server/dispatch-as-plurnk.js.map +1 -1
- package/dist/server/drain.sql +27 -27
- package/dist/server/envelope.d.ts +20 -20
- package/dist/server/envelope.d.ts.map +1 -1
- package/dist/server/envelope.js +86 -86
- package/dist/server/envelope.js.map +1 -1
- package/dist/server/envelope.sql +39 -39
- package/dist/server/logEntry.d.ts +1 -1
- package/dist/server/logEntry.d.ts.map +1 -1
- package/dist/server/logEntry.js +1 -1
- package/dist/server/logEntry.js.map +1 -1
- package/dist/server/loop-model.d.ts +3 -0
- package/dist/server/loop-model.d.ts.map +1 -0
- package/dist/server/loop-model.js +26 -0
- package/dist/server/loop-model.js.map +1 -0
- package/dist/server/loopDocs.d.ts +1 -1
- package/dist/server/loopDocs.d.ts.map +1 -1
- package/dist/server/loopDocs.js +6 -6
- package/dist/server/loopDocs.js.map +1 -1
- package/dist/server/seam-entry-read.sql +2 -2
- package/dist/server/seam-log-read.sql +3 -3
- package/dist/server/seam-loop.sql +3 -3
- package/dist/server/seam-proposal-list.sql +4 -4
- package/dist/server/yolo.js +2 -2
- package/dist/server/yolo.js.map +1 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +21 -6
- package/dist/service.js.map +1 -1
- package/migrations/0000-00-00.01_schema.sql +84 -85
- package/package.json +17 -14
- package/dist/core/TokenGauge.d.ts +0 -16
- package/dist/core/TokenGauge.d.ts.map +0 -1
- package/dist/core/TokenGauge.js +0 -58
- package/dist/core/TokenGauge.js.map +0 -1
- package/dist/core/run-cap.d.ts +0 -8
- package/dist/core/run-cap.d.ts.map +0 -1
- package/dist/core/run-cap.js +0 -20
- package/dist/core/run-cap.js.map +0 -1
- package/dist/core/search-prefetch.d.ts +0 -17
- package/dist/core/search-prefetch.d.ts.map +0 -1
- package/dist/core/search-prefetch.js +0 -164
- package/dist/core/search-prefetch.js.map +0 -1
- package/dist/core/session-settings.d.ts.map +0 -1
- package/dist/core/session-settings.js.map +0 -1
- package/dist/schemes/Run.d.ts.map +0 -1
- package/dist/schemes/Run.js.map +0 -1
package/dist/core/Engine.js
CHANGED
|
@@ -2,14 +2,13 @@ import { PlurnkParser, PlurnkParseError } from "@plurnk/plurnk-grammar";
|
|
|
2
2
|
import { Mimetypes, emptyRegistry } from "@plurnk/plurnk-mimetypes";
|
|
3
3
|
import EntryCrud from "../schemes/_entry-crud.js";
|
|
4
4
|
import EntryManifest from "../schemes/_entry-manifest.js";
|
|
5
|
-
import { markTerminal } from "../schemes/
|
|
6
|
-
import TokenGauge from "./TokenGauge.js";
|
|
5
|
+
import { markTerminal } from "../schemes/Worker.js";
|
|
7
6
|
import GitMembership from "./git-membership.js";
|
|
8
7
|
import GitState from "./git-state.js";
|
|
9
|
-
import
|
|
8
|
+
import WorkspaceSettings from "./workspace-settings.js";
|
|
10
9
|
import { editedSpan } from "../content/index.js";
|
|
11
10
|
import { promptPathname } from "./plurnk-uri.js";
|
|
12
|
-
import
|
|
11
|
+
import { rulerCount } from "./token-ruler.js";
|
|
13
12
|
import SearchGate from "./search-gate.js";
|
|
14
13
|
import { readFile } from "node:fs/promises";
|
|
15
14
|
import { fileURLToPath } from "node:url";
|
|
@@ -27,12 +26,11 @@ import PacketBuilder from "./PacketBuilder.js";
|
|
|
27
26
|
import ProposalLifecycle from "./ProposalLifecycle.js";
|
|
28
27
|
import Dispatcher from "./Dispatcher.js";
|
|
29
28
|
const DEFAULT_MAX_STRIKES = 3;
|
|
30
|
-
|
|
31
|
-
// The foisted prompt EDIT/READ target — run-qualified storage (§prompt-auto-read, #382 fault-1)
|
|
29
|
+
// The foisted prompt EDIT/READ target — worker-qualified storage (§prompt-auto-read, #382 fault-1)
|
|
32
30
|
// rendered in the plurnk:// authority form (hostname carries the namespace, plurnk-uri folds it
|
|
33
31
|
// back into the storage key on dispatch).
|
|
34
|
-
const promptTarget = (
|
|
35
|
-
const storage = promptPathname(
|
|
32
|
+
const promptTarget = (workerId, loopSeq, turnSeq) => {
|
|
33
|
+
const storage = promptPathname(workerId, loopSeq, turnSeq);
|
|
36
34
|
return {
|
|
37
35
|
kind: "url", raw: `plurnk://${storage.slice(1)}`,
|
|
38
36
|
scheme: "plurnk", username: null, password: null,
|
|
@@ -49,13 +47,17 @@ const readMaxStrikes = () => {
|
|
|
49
47
|
return DEFAULT_MAX_STRIKES;
|
|
50
48
|
return n;
|
|
51
49
|
};
|
|
50
|
+
// Per-emission op ceiling — OFF by default. `-1` (or unset/non-positive) = no cap: every
|
|
51
|
+
// generated op dispatches. Runaway degeneration is a sampler concern (repetition penalty),
|
|
52
|
+
// not grounds to drop already-generated work. A positive value is an operator ceiling a
|
|
53
|
+
// workspace's maxCommands may tighten (min wins), never widen (#232).
|
|
52
54
|
const readMaxCommands = () => {
|
|
53
55
|
const raw = process.env.PLURNK_SERVICE_MAX_COMMANDS;
|
|
54
56
|
if (raw === undefined || raw.length === 0)
|
|
55
|
-
return
|
|
57
|
+
return Number.POSITIVE_INFINITY;
|
|
56
58
|
const n = Number.parseInt(raw, 10);
|
|
57
59
|
if (!Number.isFinite(n) || n < 1)
|
|
58
|
-
return
|
|
60
|
+
return Number.POSITIVE_INFINITY;
|
|
59
61
|
return n;
|
|
60
62
|
};
|
|
61
63
|
// PLURNK_SERVICE_FILES_ITEMS — the turn-0 manifest preview. null = off (no foist);
|
|
@@ -115,7 +117,7 @@ export default class Engine {
|
|
|
115
117
|
// marker's seconds; -1 = indefinite), consumed by the daemon's drain park-exit to schedule
|
|
116
118
|
// the deadline wake. In-memory: a daemon restart drops pending deadlines (documented).
|
|
117
119
|
parkDeadlines = new Map();
|
|
118
|
-
// §join-blocking-collect (#354) — loops whose turn issued a READ(
|
|
120
|
+
// §join-blocking-collect (#354) — loops whose turn issued a READ(worker://running-child): the
|
|
119
121
|
// blocking join. The dispatcher arms this on the READ; the turn's bare SEND[102] parks on it
|
|
120
122
|
// (indefinite) instead of continuing, and any SEND clears it. Twin of parkDeadlines.
|
|
121
123
|
joinTargets = new Set();
|
|
@@ -128,7 +130,6 @@ export default class Engine {
|
|
|
128
130
|
// fitting turn (the model curated; a LATER overflow earns a fresh recovery) and at loop cleanup.
|
|
129
131
|
#hardOverflowRecovery = new Set();
|
|
130
132
|
#packets;
|
|
131
|
-
searchPrefetch;
|
|
132
133
|
searchGate = new SearchGate();
|
|
133
134
|
#proposals;
|
|
134
135
|
#dispatcher;
|
|
@@ -138,20 +139,15 @@ export default class Engine {
|
|
|
138
139
|
// Streaming schemes (exec) chain their per-spawn controllers off
|
|
139
140
|
// ctx.signal so cancelled loops tear down their background spawns.
|
|
140
141
|
#loopAborts = new Map();
|
|
141
|
-
// §tokenomics-ceiling-calibrates-to-usage (#311) — the loop's observed real/measured token
|
|
142
|
-
// ratio (provider usage.prompt ÷ our measured packet tokens), monotone max so the ceiling
|
|
143
|
-
// only tightens. A chars/4 heuristic ruler undercounts escaped-JSON logs ~1.5×; calibrating
|
|
144
|
-
// against the provider's own count makes a real context overflow unreachable past turn 1.
|
|
145
|
-
#tokenRatios = new Map();
|
|
146
142
|
// §send-premature-terminate — loops owed one idle-grace turn after a retrieval-only 409
|
|
147
143
|
// (the steer's own advice is to wait; in-memory, fail-open on restart).
|
|
148
144
|
#retrievalRefusalGrace = new Set();
|
|
149
145
|
// §derivation-off-hot-path — the background derivation chain: the per-turn pump and the
|
|
150
|
-
//
|
|
146
|
+
// workspace warm ride it instead of the turn (a 2-CPU container CPU-embedding a 335-entry
|
|
151
147
|
// ingest starved every loop for ~28min, #316). Serialized (never two pumps interleaved),
|
|
152
148
|
// drained at daemon stop (never racing db close), failures logged (never swallowed).
|
|
153
149
|
// A turn never waits on an embedding; a ~query warms its own candidate slice inline
|
|
154
|
-
// (§semantic-cold-query-full-fidelity), so cold
|
|
150
|
+
// (§semantic-cold-query-full-fidelity), so cold workspaces still get full-fidelity search.
|
|
155
151
|
#derivationChain = Promise.resolve();
|
|
156
152
|
#queueDerivation(job) {
|
|
157
153
|
const run = this.#derivationChain.then(job).catch((err) => {
|
|
@@ -165,14 +161,15 @@ export default class Engine {
|
|
|
165
161
|
await this.#derivationChain;
|
|
166
162
|
}
|
|
167
163
|
#streamEventNotify;
|
|
168
|
-
#
|
|
164
|
+
#wakeWorkerNotify;
|
|
169
165
|
// Cached plurnk GBNF — read once on the first constrained generate (#189).
|
|
170
166
|
#gbnfCache = new Map(); // variant name -> GBNF text (per-alias selection, #353)
|
|
171
|
-
|
|
167
|
+
#railAnnounced = new Set(); // #488 — one rail-state line per alias, positive drill-visible signal
|
|
168
|
+
constructor({ db, schemes, mimetypes, streamEventNotify, wakeWorkerNotify, injectWorker, cancelWorker, telemetryEventNotify, tokenize }) {
|
|
172
169
|
this.#db = db;
|
|
173
170
|
this.#schemes = schemes;
|
|
174
171
|
this.#streamEventNotify = streamEventNotify;
|
|
175
|
-
this.#
|
|
172
|
+
this.#wakeWorkerNotify = wakeWorkerNotify;
|
|
176
173
|
// Default to empty discovery — standalone Engine construction (in
|
|
177
174
|
// tests) gets no handlers, and content flows through the framework's
|
|
178
175
|
// raw-content fitContent fallback. Daemon-managed Engine receives a
|
|
@@ -183,18 +180,15 @@ export default class Engine {
|
|
|
183
180
|
// Tripwire default matches the Mimetypes boot affordance (SPEC §mimetype-surface):
|
|
184
181
|
// the divisor stands in only until the provider-backed tokenizer is
|
|
185
182
|
// wired by the Daemon. Real counts come from provider.countTokens.
|
|
186
|
-
this.#tokenize = tokenize ??
|
|
183
|
+
this.#tokenize = tokenize ?? rulerCount;
|
|
187
184
|
const executors = () => this.#executors;
|
|
188
185
|
const loopSignal = (loopId) => this.#loopAborts.get(loopId)?.signal;
|
|
189
186
|
this.#telemetry = new TelemetryChannel({ db, notify: telemetryEventNotify });
|
|
190
187
|
this.#strikes = new StrikeRail();
|
|
191
188
|
this.#packets = new PacketBuilder({ db, schemes, telemetry: this.#telemetry, executors });
|
|
192
|
-
// §search-prefetch — the web-member sync pass; the daemon's wake handler drives it,
|
|
193
|
-
// the hold loop below honors it (holds() = a pass in flight for the run).
|
|
194
|
-
this.searchPrefetch = new SearchPrefetch({ db: this.#db, schemes: this.#schemes, tokenize: this.#tokenize });
|
|
195
189
|
this.#proposals = new ProposalLifecycle({
|
|
196
190
|
db, schemes, telemetry: this.#telemetry,
|
|
197
|
-
streamEventNotify,
|
|
191
|
+
streamEventNotify, wakeWorkerNotify,
|
|
198
192
|
tokenize: this.#tokenize, mimetypes: this.#mimetypes, executors, loopSignal,
|
|
199
193
|
});
|
|
200
194
|
this.#dispatcher = new Dispatcher({ searchGate: this.searchGate,
|
|
@@ -202,7 +196,7 @@ export default class Engine {
|
|
|
202
196
|
tokenize: this.#tokenize,
|
|
203
197
|
telemetry: this.#telemetry, proposals: this.#proposals,
|
|
204
198
|
executors, loopSignal,
|
|
205
|
-
streamEventNotify,
|
|
199
|
+
streamEventNotify, wakeWorkerNotify, injectWorker, cancelWorker,
|
|
206
200
|
parkDeadlines: this.parkDeadlines,
|
|
207
201
|
joinTargets: this.joinTargets,
|
|
208
202
|
});
|
|
@@ -243,18 +237,36 @@ export default class Engine {
|
|
|
243
237
|
// that constrain sampling (llama-server, response_format backends); a cloud model that IGNORES the grammar
|
|
244
238
|
// gets a filter-mode divergence event every turn for nothing. So the bare default is OFF
|
|
245
239
|
// and the GBNF-capable aliases opt IN via a PLURNK_PROVIDERS_GBNF_<alias> suffix.
|
|
246
|
-
|
|
240
|
+
// #488 — the rail must be VERIFIABLE, never silently off. Two guards:
|
|
241
|
+
// (1) the alias fallback is only trusted when NO per-alias GBNF opt-ins exist: an
|
|
242
|
+
// unregistered provider in a process that configured suffixed rails could fall back
|
|
243
|
+
// to a DIFFERENT active alias, miss the suffix, and run unconstrained — the silent
|
|
244
|
+
// severance class run78 demonstrated (free decode, fabricated logs, CLEAN telemetry).
|
|
245
|
+
// (2) every resolution states itself ONCE on stderr — "grammar rail: <alias> → <variant>/OFF" —
|
|
246
|
+
// so any drill capture positively shows rail state; absence of the line is greppable.
|
|
247
|
+
const registered = ProviderInstantiate.aliasOf(provider);
|
|
248
|
+
const fallback = registered === undefined ? resolveActiveAlias(process.env)?.alias : undefined;
|
|
249
|
+
if (registered === undefined && fallback === undefined && Object.keys(process.env).some((k) => k.startsWith("PLURNK_PROVIDERS_GBNF_"))) {
|
|
250
|
+
throw new Error("grammar rail: provider has no registered alias and no active alias resolves, while per-alias PLURNK_PROVIDERS_GBNF_* rails are configured — refusing to run unconstrained (#488).");
|
|
251
|
+
}
|
|
252
|
+
const alias = registered ?? fallback ?? "";
|
|
247
253
|
const variant = scopeEnvToAlias(process.env, alias, ["PLURNK_PROVIDERS_GBNF"]).PLURNK_PROVIDERS_GBNF;
|
|
248
|
-
if (variant === undefined || variant === "" || variant === "0")
|
|
254
|
+
if (variant === undefined || variant === "" || variant === "0") {
|
|
255
|
+
if (!this.#railAnnounced.has(alias)) {
|
|
256
|
+
this.#railAnnounced.add(alias);
|
|
257
|
+
process.stderr.write(`plurnk-engine: grammar rail: ${alias || "(bare)"} → OFF (no PLURNK_PROVIDERS_GBNF for this alias)\n`);
|
|
258
|
+
}
|
|
249
259
|
return undefined;
|
|
260
|
+
}
|
|
250
261
|
const hit = this.#gbnfCache.get(variant);
|
|
251
262
|
if (hit !== undefined)
|
|
252
263
|
return hit;
|
|
253
264
|
const path = variant.startsWith("/") || variant.startsWith(".")
|
|
254
265
|
? variant
|
|
255
266
|
: fileURLToPath(import.meta.resolve(`@plurnk/plurnk-grammar/${variant}`));
|
|
256
|
-
const text = await readFile(path, "utf8");
|
|
267
|
+
const text = await readFile(path, "utf8"); // unresolvable/unreadable throws — a configured rail never silently degrades
|
|
257
268
|
this.#gbnfCache.set(variant, text);
|
|
269
|
+
process.stderr.write(`plurnk-engine: grammar rail: ${alias || "(bare)"} → ${variant} (${text.length} chars)\n`);
|
|
258
270
|
return text;
|
|
259
271
|
}
|
|
260
272
|
// Per-loop usage totals (#197): SUM the loop's turns (usage is stored per
|
|
@@ -277,7 +289,7 @@ export default class Engine {
|
|
|
277
289
|
// summed promptTokens above, which overcounts a context that grows across turns.
|
|
278
290
|
contextTokens: row?.context ?? 0,
|
|
279
291
|
// #274 — the last turn's model window (denominator); null when the provider reports none.
|
|
280
|
-
|
|
292
|
+
promptBudget: row?.context_size ?? null,
|
|
281
293
|
// #252 — the latest turn's opaque provider blob, parsed for the wire. Empty {} when the
|
|
282
294
|
// provider returned no meta. The service forwards it; it never reads a field within.
|
|
283
295
|
meta: JSON.parse(row?.meta ?? "{}"),
|
|
@@ -303,7 +315,7 @@ export default class Engine {
|
|
|
303
315
|
}
|
|
304
316
|
return { line, column };
|
|
305
317
|
}
|
|
306
|
-
async runLoop({ provider, messages, requirements = "",
|
|
318
|
+
async runLoop({ provider, messages, requirements = "", workspaceId, workerId, loopId, maxTurns = 50, maxStrikes = readMaxStrikes(), minCycles = readPositiveInt("PLURNK_SERVICE_MIN_CYCLES", DEFAULT_MIN_CYCLES), maxCyclePeriod = readPositiveInt("PLURNK_SERVICE_MAX_CYCLE_PERIOD", DEFAULT_MAX_CYCLE_PERIOD), origin = "model", signal, onDispatch, }) {
|
|
307
319
|
const turnIds = [];
|
|
308
320
|
const suddenDeathThreshold = maxTurns - maxStrikes;
|
|
309
321
|
// Per-loop AbortController for scheme-side cancellation propagation.
|
|
@@ -345,7 +357,6 @@ export default class Engine {
|
|
|
345
357
|
this.searchGate.cleanup(loopId);
|
|
346
358
|
this.#hardOverflowRecovery.delete(loopId);
|
|
347
359
|
this.#telemetry.delete(loopId);
|
|
348
|
-
this.#tokenRatios.delete(loopId);
|
|
349
360
|
};
|
|
350
361
|
while (true) {
|
|
351
362
|
// The wall fired between turns — rule 504 before anything else reads the loop.
|
|
@@ -358,7 +369,7 @@ export default class Engine {
|
|
|
358
369
|
if (row.status === 100) {
|
|
359
370
|
// NOT a terminal — a wake re-queued this loop while its own live drain was
|
|
360
371
|
// between turns (a child concluded in the gap between our 202 write and this
|
|
361
|
-
// check, §
|
|
372
|
+
// check, §worker-lifecycle-wake-requeue-not-terminal). The wake's intent is KEEP
|
|
362
373
|
// RUNNING: re-claim atomically and continue — the injected prompt is already
|
|
363
374
|
// this loop's next turn. Returning it as "external" broadcast a QUEUED loop
|
|
364
375
|
// as loop/terminated {finalStatus: 100} — the delegation-flags flake.
|
|
@@ -396,19 +407,19 @@ export default class Engine {
|
|
|
396
407
|
const holdCapMs = Number(process.env.PLURNK_SERVICE_EXEC_HOLD_MS ?? "300000");
|
|
397
408
|
if (holdSet.size > 0 && holdCapMs > 0 && execHandler?.hasActiveHoldSpawns !== undefined) {
|
|
398
409
|
const holdStart = Date.now();
|
|
399
|
-
while (
|
|
410
|
+
while (execHandler.hasActiveHoldSpawns(workerId, holdSet) && Date.now() - holdStart < holdCapMs) {
|
|
400
411
|
await delay(150, undefined, { signal });
|
|
401
412
|
}
|
|
402
413
|
}
|
|
403
414
|
const execWaitMs = Number(process.env.PLURNK_SERVICE_EXEC_WAIT_MS ?? "0");
|
|
404
415
|
if (execWaitMs > 0) {
|
|
405
|
-
if (execHandler?.hasActiveSpawns?.(
|
|
416
|
+
if (execHandler?.hasActiveSpawns?.(workerId) === true)
|
|
406
417
|
await delay(execWaitMs, undefined, { signal });
|
|
407
418
|
}
|
|
408
419
|
let turn;
|
|
409
420
|
try {
|
|
410
421
|
turn = await this.runTurn({
|
|
411
|
-
provider, messages, requirements,
|
|
422
|
+
provider, messages, requirements, workspaceId, workerId, loopId, origin, signal, onDispatch,
|
|
412
423
|
turnNumber: turnIds.length + 1, maxTurns,
|
|
413
424
|
});
|
|
414
425
|
}
|
|
@@ -455,7 +466,7 @@ export default class Engine {
|
|
|
455
466
|
}
|
|
456
467
|
}
|
|
457
468
|
}
|
|
458
|
-
async runTurn({ provider, messages, requirements = "",
|
|
469
|
+
async runTurn({ provider, messages, requirements = "", workspaceId, workerId, loopId, origin = "model", signal, onDispatch, turnNumber = 1, maxTurns = 50, }) {
|
|
459
470
|
// === Turn-as-container model ===
|
|
460
471
|
//
|
|
461
472
|
// Turn rows are created at runTurn OPEN (status=102, placeholder
|
|
@@ -468,8 +479,8 @@ export default class Engine {
|
|
|
468
479
|
// writes consume low indices; model ops continue from there.
|
|
469
480
|
const seqRow = await this.#db.engine_next_turn_sequence.get({ loop_id: loopId });
|
|
470
481
|
const seq = seqRow.next;
|
|
471
|
-
// #269 — loops.sequence is the loop's ordinal WITHIN the
|
|
472
|
-
// RUN (manifest preview, AGENTS, operator docs) gate on the
|
|
482
|
+
// #269 — loops.sequence is the loop's ordinal WITHIN the worker. Turn-0 foists that belong to the
|
|
483
|
+
// RUN (manifest preview, AGENTS, operator docs) gate on the worker's FIRST loop, not every loop's
|
|
473
484
|
// first turn; per-loop foists (the prompt, @file) still fire each loop. Read once, turn-1 only.
|
|
474
485
|
const loopRow = seq === 1
|
|
475
486
|
? await this.#db.engine_get_loop_prompt.get({ loop_id: loopId })
|
|
@@ -493,7 +504,7 @@ export default class Engine {
|
|
|
493
504
|
// from sequence=2 onward on prompt-foisted turns; 1 onward
|
|
494
505
|
// otherwise.
|
|
495
506
|
let nextActionIndex = 1;
|
|
496
|
-
// §model-entry — the
|
|
507
|
+
// §model-entry — the worker's first turn opens with the model's own turn-0, mirrored OPEN: a
|
|
497
508
|
// worked turn PLAN → the environment FINDs the foist ACTUALLY dispatches → SEND[102]. Built
|
|
498
509
|
// from the real ops below (not a static print — we lean into the genuine echo paradigm) and
|
|
499
510
|
// written at sequence 1, so it reads first as the emission with the foisted results following.
|
|
@@ -502,15 +513,15 @@ export default class Engine {
|
|
|
502
513
|
if (runFirstLoop)
|
|
503
514
|
nextActionIndex = 2; // reserve sequence 1 for the turn-0 echo
|
|
504
515
|
// Operator doc READs (PLURNK_SERVICE_MD_<ALIAS>, §actor-boundary-doc-injection). The docs were materialized
|
|
505
|
-
// as plurnk:///<entry> entries by the plurnk
|
|
516
|
+
// as plurnk:///<entry> entries by the plurnk worker (loop_run, via the
|
|
506
517
|
// §actor-boundary keystone); foist a READ of each into THIS turn-0 so the model
|
|
507
518
|
// reads them inline. It sees only the READ — the materializing EDIT
|
|
508
|
-
// lives in the plurnk
|
|
509
|
-
// #231 — env docs (PLURNK_SERVICE_MD_*) UNION the
|
|
519
|
+
// lives in the plurnk worker's log, never the model's.
|
|
520
|
+
// #231 — env docs (PLURNK_SERVICE_MD_*) UNION the workspace's client docs; foist a READ of
|
|
510
521
|
// each materialized plurnk:///<alias>.md (loop_run materialized the same set).
|
|
511
|
-
const { mdDocs } = await
|
|
512
|
-
// #269 — operator docs are run-once; foist them only on the
|
|
513
|
-
for (const doc of runFirstLoop ? await
|
|
522
|
+
const { mdDocs } = await WorkspaceSettings.read(this.#db, workspaceId);
|
|
523
|
+
// #269 — operator docs are run-once; foist them only on the worker's first loop.
|
|
524
|
+
for (const doc of runFirstLoop ? await WorkspaceSettings.resolveDocs(mdDocs) : []) {
|
|
514
525
|
const docTarget = {
|
|
515
526
|
kind: "url", raw: `plurnk:///${doc.entryName}`, scheme: "plurnk",
|
|
516
527
|
username: null, password: null, hostname: null, port: null,
|
|
@@ -521,7 +532,7 @@ export default class Engine {
|
|
|
521
532
|
lineMarker: null, body: null, position: { line: 1, column: 1 },
|
|
522
533
|
};
|
|
523
534
|
await this.dispatch({
|
|
524
|
-
statement: docRead,
|
|
535
|
+
statement: docRead, workspaceId, workerId, loopId, turnId,
|
|
525
536
|
sequence: nextActionIndex, origin: "plurnk", onDispatch,
|
|
526
537
|
});
|
|
527
538
|
nextActionIndex++;
|
|
@@ -529,7 +540,7 @@ export default class Engine {
|
|
|
529
540
|
const promptRow = loopRow; // #269 — already read above (per-loop; fires every loop's turn 1)
|
|
530
541
|
if (promptRow !== undefined && typeof promptRow.prompt === "string" && promptRow.prompt.length > 0) {
|
|
531
542
|
const promptLoopSeq = promptRow.sequence; // the loop's PER-RUN sequence — model-facing, matching log coordinates (owner: the db id read as prompt/2/1)
|
|
532
|
-
const promptPath = promptTarget(
|
|
543
|
+
const promptPath = promptTarget(workerId, promptLoopSeq, seq);
|
|
533
544
|
const promptStmt = {
|
|
534
545
|
op: "EDIT", suffix: "", signal: null,
|
|
535
546
|
target: promptPath, lineMarker: null,
|
|
@@ -537,7 +548,7 @@ export default class Engine {
|
|
|
537
548
|
};
|
|
538
549
|
let promptLogId;
|
|
539
550
|
await this.dispatch({
|
|
540
|
-
statement: promptStmt,
|
|
551
|
+
statement: promptStmt, workspaceId, workerId, loopId, turnId,
|
|
541
552
|
sequence: nextActionIndex, origin: "plurnk",
|
|
542
553
|
onDispatch: (id) => { promptLogId = id; onDispatch?.(id); },
|
|
543
554
|
});
|
|
@@ -551,14 +562,19 @@ export default class Engine {
|
|
|
551
562
|
// when it runs fewer than 12 (whole-read form doubles as teaching). Prior prompts
|
|
552
563
|
// stay listed by path in the system packet's User Prompts section — reachable,
|
|
553
564
|
// never silently lost.
|
|
565
|
+
// §arrival-law (#499) — the preview bound is the knob (was a hardcoded 12). The
|
|
566
|
+
// line-slice is an EFFICIENCY (do not materialize hundreds of lines into rx); the
|
|
567
|
+
// RENDER is the law — packet-wire's arrival preview cuts the char dimension there,
|
|
568
|
+
// which a line marker structurally cannot (a 1-line slice of a 1-line bomb is 416).
|
|
569
|
+
const previewLines = Number(process.env.PLURNK_SERVICE_ARRIVAL_PREVIEW_LINES ?? "16");
|
|
554
570
|
const promptLineCount = promptRow.prompt.split("\n").length;
|
|
555
571
|
const promptRead = {
|
|
556
572
|
op: "READ", suffix: "", signal: null, target: promptPath,
|
|
557
|
-
lineMarker: { marks: promptLineCount >=
|
|
573
|
+
lineMarker: { marks: promptLineCount >= previewLines ? [1, previewLines] : [1, -1] },
|
|
558
574
|
body: null, position: { line: 1, column: 1 },
|
|
559
575
|
};
|
|
560
576
|
await this.dispatch({
|
|
561
|
-
statement: promptRead,
|
|
577
|
+
statement: promptRead, workspaceId, workerId, loopId, turnId,
|
|
562
578
|
sequence: nextActionIndex, origin: "plurnk", onDispatch,
|
|
563
579
|
});
|
|
564
580
|
nextActionIndex++;
|
|
@@ -570,18 +586,18 @@ export default class Engine {
|
|
|
570
586
|
if (seq > 1) {
|
|
571
587
|
const loopSeqRow = await this.#db.engine_loop_sequence.get({ loop_id: loopId });
|
|
572
588
|
const loopSeq = loopSeqRow?.sequence ?? loopId;
|
|
573
|
-
const injected = await this.#db.drain_get_all_prompt_bodies_for_loop.all({ pattern: promptPathname(
|
|
589
|
+
const injected = await this.#db.drain_get_all_prompt_bodies_for_loop.all({ pattern: promptPathname(workerId, loopSeq, seq) });
|
|
574
590
|
const injectedRow = injected.find((r) => typeof r.content === "string" && r.content.length > 0);
|
|
575
591
|
if (injectedRow !== undefined) {
|
|
576
592
|
const lineCount = injectedRow.content.split("\n").length;
|
|
577
|
-
const injTarget = promptTarget(
|
|
593
|
+
const injTarget = promptTarget(workerId, loopSeq, seq);
|
|
578
594
|
const injRead = {
|
|
579
595
|
op: "READ", suffix: "", signal: null, target: injTarget,
|
|
580
596
|
lineMarker: { marks: lineCount >= 12 ? [1, 12] : [1, -1] },
|
|
581
597
|
body: null, position: { line: 1, column: 1 },
|
|
582
598
|
};
|
|
583
599
|
await this.dispatch({
|
|
584
|
-
statement: injRead,
|
|
600
|
+
statement: injRead, workspaceId, workerId, loopId, turnId,
|
|
585
601
|
sequence: nextActionIndex, origin: "plurnk", onDispatch,
|
|
586
602
|
});
|
|
587
603
|
nextActionIndex++;
|
|
@@ -591,48 +607,46 @@ export default class Engine {
|
|
|
591
607
|
// every entry's deep channels (symbols/refs/embeddings/FTS, deep_hash-gated) so the
|
|
592
608
|
// catalog and FIND read current data. NOT an action: no log entry, no sequence slot,
|
|
593
609
|
// not dispatched. There is no plurnk:///manifest.json entry — the catalog is served
|
|
594
|
-
// on demand by FIND(scheme:///**), foisted into the
|
|
610
|
+
// on demand by FIND(scheme:///**), foisted into the worker's first turn below.
|
|
595
611
|
// #312 — the turn's token gauge: the ACTIVE provider's tokenizer identity + exact counter
|
|
596
612
|
// (mimetypes seam; provider upper bound surfaced as tokenizer_unavailable when inexact).
|
|
597
613
|
// Threaded per turn — never engine state — so concurrent loops on different providers
|
|
598
614
|
// each read their own honest numbers.
|
|
599
|
-
const gauge = await TokenGauge.resolve(this.#mimetypes, provider, (event) => this.#telemetry.push(sessionId, loopId, event));
|
|
600
615
|
const systemCtx = {
|
|
601
|
-
db: this.#db,
|
|
616
|
+
db: this.#db, workspaceId, workerId, loopId, turnId,
|
|
602
617
|
writer: "plurnk",
|
|
603
618
|
signal: this.#loopAborts.get(loopId)?.signal,
|
|
604
619
|
streamEventNotify: this.#streamEventNotify,
|
|
605
|
-
|
|
620
|
+
wakeWorkerNotify: this.#wakeWorkerNotify,
|
|
606
621
|
tokenize: this.#tokenize,
|
|
607
|
-
gauge,
|
|
608
622
|
mimetypes: this.#mimetypes,
|
|
609
623
|
defaultChannelFor: (s) => this.#schemes.defaultChannelFor(s),
|
|
610
|
-
pushTelemetry: (event) => this.#telemetry.push(
|
|
624
|
+
pushTelemetry: (event) => this.#telemetry.push(workspaceId, loopId, event),
|
|
611
625
|
};
|
|
612
626
|
// SPEC §membership D4/D5 — git-ls-files workspace membership, resolved at
|
|
613
627
|
// prompt-composition (EMI is eager + exhaustive — git is the only bound). When the
|
|
614
|
-
//
|
|
628
|
+
// workspace's project_root is a git working tree, tracked files are
|
|
615
629
|
// members without a client `add`; active members are materialized
|
|
616
630
|
// (disk → body channel) so they appear in the catalog. No-ops
|
|
617
|
-
// on headless / non-git
|
|
631
|
+
// on headless / non-git workspaces. Runs BEFORE the derivation pump so
|
|
618
632
|
// this turn's packet reflects them.
|
|
619
633
|
const fsDivergences = await GitMembership.indexGitMembership(systemCtx);
|
|
620
|
-
await this.#logFsFictions(
|
|
634
|
+
await this.#logFsFictions(workspaceId, fsDivergences);
|
|
621
635
|
this.#queueDerivation(() => EntryManifest.maintainDerivations(systemCtx)); // §derivation-off-hot-path — the turn proceeds; ~queries warm their own slice
|
|
622
636
|
// Turn-0 catalog preview (PLURNK_SERVICE_FILES_ITEMS, §actor-boundary-catalog-preview):
|
|
623
|
-
// one FIND(scheme:///**) per scheme that holds entries, foisted into the
|
|
637
|
+
// one FIND(scheme:///**) per scheme that holds entries, foisted into the worker's first
|
|
624
638
|
// model turn so it opens with its catalog (the per-scheme arrays that replaced the
|
|
625
639
|
// single manifest.json). -1 → each scheme's whole catalog; N → its first N rows
|
|
626
640
|
// (clamped to the scheme's count so FIND's strict <L> never 416s); off by default.
|
|
627
641
|
if (seq === 1) {
|
|
628
|
-
// #231 — a
|
|
629
|
-
const { filesItems:
|
|
630
|
-
const filesItems =
|
|
642
|
+
// #231 — a workspace's client-chosen filesItems REPLACES the env default outright.
|
|
643
|
+
const { filesItems: workspaceMI } = await WorkspaceSettings.read(this.#db, workspaceId);
|
|
644
|
+
const filesItems = workspaceMI !== null ? normalizeFilesItems(workspaceMI) : readFilesItems();
|
|
631
645
|
if (filesItems !== null && runFirstLoop) { // #269 — catalog preview is run-once
|
|
632
|
-
// engine_scheme_catalog_summary is the scheme source:
|
|
646
|
+
// engine_scheme_catalog_summary is the scheme source: workspace-scoped, ordered,
|
|
633
647
|
// one row per scheme that has entries (scheme=null → file). log:// is absent —
|
|
634
648
|
// it lives in log_entries, not the catalog (present-mode, the # Log section).
|
|
635
|
-
const catalogSchemes = await this.#db.engine_scheme_catalog_summary.all({
|
|
649
|
+
const catalogSchemes = await this.#db.engine_scheme_catalog_summary.all({ workspace_id: workspaceId });
|
|
636
650
|
// known:/// + unknown:/// + file ALWAYS foist, even at zero entries — else the
|
|
637
651
|
// model burns a turn running the FIND itself, assuming the catalog is merely
|
|
638
652
|
// being withheld. An empty FIND(**) is orienting, not noise (owner): it tells
|
|
@@ -674,7 +688,7 @@ export default class Engine {
|
|
|
674
688
|
position: { line: 1, column: 1 },
|
|
675
689
|
};
|
|
676
690
|
await this.dispatch({
|
|
677
|
-
statement: catalogFind,
|
|
691
|
+
statement: catalogFind, workspaceId, workerId, loopId, turnId,
|
|
678
692
|
sequence: nextActionIndex, origin: "plurnk", onDispatch,
|
|
679
693
|
});
|
|
680
694
|
nextActionIndex++;
|
|
@@ -682,21 +696,21 @@ export default class Engine {
|
|
|
682
696
|
// own survey, mirrored OPEN). The <L> cap rides as `<1,N>`, exactly as the model would type it.
|
|
683
697
|
turnZeroMoves.push(`<<FIND(${isPlurnk ? "plurnk://docs/**" : isFile ? "**" : `${schemeName}:///**`})${cap === null ? "" : `<1,${cap}>`}::FIND`);
|
|
684
698
|
}
|
|
685
|
-
// §
|
|
686
|
-
// building
|
|
699
|
+
// §worker-scheme — Manifest(run) = workspace-scope ∪ THIS worker's worker-scope. Foist the
|
|
700
|
+
// building worker's OWN scratch (worker://self/**, uncapped — a worker needs the full view to
|
|
687
701
|
// manage its private workspace) so it's catalogued in ITS perspective alone; other
|
|
688
|
-
// runs reach it only via explicit FIND(
|
|
689
|
-
const
|
|
690
|
-
const scratch =
|
|
702
|
+
// runs reach it only via explicit FIND(worker://<name>/**). A worker with no scratch foists nothing.
|
|
703
|
+
const selfWorker = await this.#db.worker_name_by_id.get({ worker_id: workerId });
|
|
704
|
+
const scratch = selfWorker === undefined ? 0 : (await this.#db.engine_worker_scratch_count.get({ workspace_id: workspaceId, owner_prefix: `/${selfWorker.name}/*` }))?.entries ?? 0;
|
|
691
705
|
if (scratch > 0) {
|
|
692
706
|
const runFind = {
|
|
693
707
|
op: "FIND", suffix: "", signal: null,
|
|
694
|
-
target: { kind: "url", raw: "
|
|
708
|
+
target: { kind: "url", raw: "worker://self/**", scheme: "worker", username: null, password: null, hostname: "self", port: null, pathname: "/**", params: {}, fragment: null },
|
|
695
709
|
body: null, lineMarker: null, position: { line: 1, column: 1 },
|
|
696
710
|
};
|
|
697
|
-
await this.dispatch({ statement: runFind,
|
|
711
|
+
await this.dispatch({ statement: runFind, workspaceId, workerId, loopId, turnId, sequence: nextActionIndex, origin: "plurnk", onDispatch });
|
|
698
712
|
nextActionIndex++;
|
|
699
|
-
turnZeroMoves.push("<<FIND(
|
|
713
|
+
turnZeroMoves.push("<<FIND(worker://self/**)::FIND"); // §model-entry — the worker-scope survey, into the turn-0 echo
|
|
700
714
|
}
|
|
701
715
|
}
|
|
702
716
|
// #260 — foist a turn-0 READ of each client-passed @file path so its content sits in front
|
|
@@ -715,7 +729,7 @@ export default class Engine {
|
|
|
715
729
|
body: null, position: { line: 1, column: 1 },
|
|
716
730
|
};
|
|
717
731
|
await this.dispatch({
|
|
718
|
-
statement: fileRead,
|
|
732
|
+
statement: fileRead, workspaceId, workerId, loopId, turnId,
|
|
719
733
|
sequence: nextActionIndex, origin: "plurnk", onDispatch,
|
|
720
734
|
});
|
|
721
735
|
nextActionIndex++;
|
|
@@ -726,35 +740,34 @@ export default class Engine {
|
|
|
726
740
|
// so the grammar can stay thin. Subsequent turns mirror the model's real output, folded.
|
|
727
741
|
if (runFirstLoop) {
|
|
728
742
|
const emission = ["<<PLAN:Initialize:PLAN", ...turnZeroMoves, "<<SEND[102]:Initialized:SEND"].join("\n");
|
|
729
|
-
await this.#dispatcher.writeModelEntry({ verbatim: emission,
|
|
743
|
+
await this.#dispatcher.writeModelEntry({ verbatim: emission, workerId, loopId, turnId, sequence: 1, folded: false, origin: "plurnk" });
|
|
730
744
|
}
|
|
731
745
|
}
|
|
732
|
-
// §env-delta — pre-seed the
|
|
746
|
+
// §env-delta — pre-seed the worker's ambient observations (what changed since
|
|
733
747
|
// it last looked) as foisted rows before the packet composes; advance the action index
|
|
734
748
|
// past them so model ops continue after. Two instances of one machine: env-delta (sibling
|
|
735
749
|
// edits · timestamp cursor · always folded) and exec streams (channel bytes · byte cursor ·
|
|
736
750
|
// terminal delta opens). §env-delta §exec-stream
|
|
737
|
-
// §exec-poll — EXEC `<0>` is turn-scoped: reap the
|
|
751
|
+
// §exec-poll — EXEC `<0>` is turn-scoped: reap the worker's open turn-scoped streams (necessarily
|
|
738
752
|
// from a prior turn — this runs before the turn's own spawns) so a `<0>` never survives into
|
|
739
753
|
// the subsequent turn. The terminal output then surfaces born-OPEN via the stream-delta path.
|
|
740
|
-
await this.#reapTurnScopedStreams(
|
|
741
|
-
nextActionIndex += await this.#materializeEnvironmentDeltas({
|
|
742
|
-
nextActionIndex += await this.#materializeStreamDeltas({
|
|
754
|
+
await this.#reapTurnScopedStreams(workerId);
|
|
755
|
+
nextActionIndex += await this.#materializeEnvironmentDeltas({ workspaceId, workerId, loopId, turnId, fromSequence: nextActionIndex });
|
|
756
|
+
nextActionIndex += await this.#materializeStreamDeltas({ workerId, loopId, turnId, fromSequence: nextActionIndex });
|
|
743
757
|
// SPEC §telemetry — git working-tree state for the telemetry section, read once
|
|
744
758
|
// (a service-side `git status` shell-out) and threaded into the budget
|
|
745
759
|
// rebuild too so it isn't re-shelled on overflow.
|
|
746
|
-
const gitStatus = await GitState.status(this.#db,
|
|
760
|
+
const gitStatus = await GitState.status(this.#db, workspaceId, this.#loopAborts.get(loopId)?.signal);
|
|
747
761
|
// Build the spec'd packet (Packet.json) request half. The log build
|
|
748
|
-
// queries log_entries scoped to the
|
|
762
|
+
// queries log_entries scoped to the worker — the prompt entry just
|
|
749
763
|
// written (if turn 1) is part of that query result.
|
|
750
|
-
const tokenRatio = this.#tokenRatios.get(loopId) ?? 1;
|
|
751
764
|
let requestPacket = await this.#packets.buildRequestPacket({
|
|
752
|
-
initialMessages: messages, requirements,
|
|
753
|
-
currentTurnSeq: seq, provider, gitStatus,
|
|
765
|
+
initialMessages: messages, requirements, workspaceId, workerId, loopId,
|
|
766
|
+
currentTurnSeq: seq, provider, gitStatus,
|
|
754
767
|
});
|
|
755
768
|
// SPEC §grinder — budget grinder, pre-LLM: reclaim window on actual overflow.
|
|
756
769
|
const enforced = await this.#packets.enforceBudget({
|
|
757
|
-
packet: requestPacket, provider,
|
|
770
|
+
packet: requestPacket, provider, workerId, loopId, turnId,
|
|
758
771
|
// The overflow error row is minted at the turn's running sequence (nextActionIndex), pre-generate;
|
|
759
772
|
// runTurn advances the counter past it below so the post-generate dispatch rows never collide.
|
|
760
773
|
mintSequence: nextActionIndex,
|
|
@@ -762,8 +775,8 @@ export default class Engine {
|
|
|
762
775
|
// overflow row just minted surfaces THIS turn (§grinder-overflow-error-row). Safe: the
|
|
763
776
|
// ephemeral buffer is empty pre-generate (events drain on the next turn's build).
|
|
764
777
|
rebuild: () => this.#packets.buildRequestPacket({
|
|
765
|
-
initialMessages: messages, requirements,
|
|
766
|
-
currentTurnSeq: seq, provider, gitStatus,
|
|
778
|
+
initialMessages: messages, requirements, workspaceId, workerId, loopId,
|
|
779
|
+
currentTurnSeq: seq, provider, gitStatus,
|
|
767
780
|
}),
|
|
768
781
|
});
|
|
769
782
|
if (enforced.struck)
|
|
@@ -780,13 +793,13 @@ export default class Engine {
|
|
|
780
793
|
// overflow terminates 413 — death only after the model was told. Physically
|
|
781
794
|
// unsendable (over the provider's real window too) → 413 immediately; physics
|
|
782
795
|
// doesn't negotiate. The pointer stays at 100% of budget — a margin would mask it.
|
|
783
|
-
const physicallySendable = provider.
|
|
796
|
+
const physicallySendable = provider.contextWindow === null
|
|
784
797
|
? true
|
|
785
|
-
:
|
|
798
|
+
: this.#packets.exactPacketTokens(requestPacket, provider) <= provider.contextWindow - this.#packets.maxTokensFor(provider);
|
|
786
799
|
if (physicallySendable && !this.#hardOverflowRecovery.has(loopId)) {
|
|
787
800
|
this.#hardOverflowRecovery.add(loopId);
|
|
788
801
|
await this.#db.engine_insert_log_entry.get({
|
|
789
|
-
|
|
802
|
+
worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence: nextActionIndex++,
|
|
790
803
|
origin: "model", source: "engine", op: "error", suffix: "", signal: null,
|
|
791
804
|
scheme: null, username: null, password: null, hostname: null, port: null,
|
|
792
805
|
pathname: null, params: null, fragment: null, lineMarker: null,
|
|
@@ -799,8 +812,8 @@ export default class Engine {
|
|
|
799
812
|
// errors sections (the same re-derive contract the soft grind uses).
|
|
800
813
|
nextActionIndex += 1;
|
|
801
814
|
requestPacket = await this.#packets.buildRequestPacket({
|
|
802
|
-
initialMessages: messages, requirements,
|
|
803
|
-
currentTurnSeq: seq, provider, gitStatus,
|
|
815
|
+
initialMessages: messages, requirements, workspaceId, workerId, loopId,
|
|
816
|
+
currentTurnSeq: seq, provider, gitStatus,
|
|
804
817
|
});
|
|
805
818
|
}
|
|
806
819
|
else {
|
|
@@ -810,7 +823,7 @@ export default class Engine {
|
|
|
810
823
|
await this.#db.engine_close_turn.run({
|
|
811
824
|
id: turnId, status: 413, packet: JSON.stringify(hardPacket),
|
|
812
825
|
usage_prompt: 0, usage_completion: 0, usage_reasoning: 0, usage_cached: 0, usage_cost_pico: 0,
|
|
813
|
-
|
|
826
|
+
usage_prompt_budget: this.#packets.promptBudgetFor(provider), // #274 — the PROMPT BUDGET (window − reserves), even on a hard-413 turn: the gauge denominator the packet actually lives under
|
|
814
827
|
finish_reason: "budget_hard_stop", model: provider.model, meta: "{}",
|
|
815
828
|
});
|
|
816
829
|
return { turnId, status: 413, statuses: [], fingerprint: "", budgetStruck: enforced.struck, budgetHardStop: true, steerStruck: false };
|
|
@@ -826,7 +839,7 @@ export default class Engine {
|
|
|
826
839
|
// the input under the ceiling); the model's decode — reasoning + emission — is
|
|
827
840
|
// out of band, owned by the provider's own context window. Deriving a maxTokens
|
|
828
841
|
// from our budget conflated the two and guillotined a reasoning model's
|
|
829
|
-
// out-of-band
|
|
842
|
+
// out-of-band reasoning as the packet filled (`ceiling - packet` → near-zero
|
|
830
843
|
// decode → finish=length mid-reasoning → no emission → strike spiral). The
|
|
831
844
|
// provider enforces its physical wall on its own.
|
|
832
845
|
let response;
|
|
@@ -836,8 +849,8 @@ export default class Engine {
|
|
|
836
849
|
// #249 — tag the loop (the activity) with its active plugins' attribution tags, write-once.
|
|
837
850
|
if (attributions.length > 0)
|
|
838
851
|
await this.#db.engine_tag_loop_attributions.run({ loop_id: loopId, attributions: JSON.stringify(attributions) });
|
|
839
|
-
// #249 —
|
|
840
|
-
const { client } = await
|
|
852
|
+
// #249 — workspace-stable frontend id, forwarded as Plurnk-Client by the plurnk provider only.
|
|
853
|
+
const { client } = await WorkspaceSettings.read(this.#db, workspaceId);
|
|
841
854
|
try {
|
|
842
855
|
// §turn-lifecycle (#301) — the provider call is the long, opaque window (submit → first
|
|
843
856
|
// committed op is provider latency + a full first-turn generation, ~70s local): a static
|
|
@@ -846,13 +859,19 @@ export default class Engine {
|
|
|
846
859
|
// ops are about to land. Base telemetry/event channel (the embed_progress precedent, §tokenomics
|
|
847
860
|
// clients already render it unconditionally); the abort guard keeps a cancelled loop silent.
|
|
848
861
|
if (!signal?.aborted)
|
|
849
|
-
this.#telemetry.push(
|
|
862
|
+
this.#telemetry.push(workspaceId, loopId, { source: "engine:turn", kind: "turn_awaiting_model", level: "info", message: "awaiting model response" });
|
|
850
863
|
// generate rides the LOOP signal (already chained from the caller's), so a loop-level
|
|
851
864
|
// abort — the §operator-config-loop-timeout wall — cancels a stuck provider call, not
|
|
852
865
|
// just the schemes. Bare runTurn (no runLoop) has no loop entry → the caller's signal.
|
|
853
|
-
|
|
866
|
+
// #391 — the turn COORDINATE (workspace + loop-seq/turn-seq of the turn being generated)
|
|
867
|
+
// rides as first-party metadata (Plurnk-Workspace-Id/Loop/Turn) so the endpoint flywheel keys
|
|
868
|
+
// on an AUTHORITATIVE hierarchy, not a scraped context-log approximation. The daemon owns
|
|
869
|
+
// the value; providers stamps the header (same split as Run-Id, #26). loopSeq (the 1-based
|
|
870
|
+
// coordinate, not the DB id) resolves the same way the prompt-slot path does (§log coords).
|
|
871
|
+
const loopSeq = (await this.#db.engine_loop_sequence.get({ loop_id: loopId }))?.sequence ?? loopId;
|
|
872
|
+
response = await provider.generate({ messages: modelMessages, workerId: String(workerId), signal: this.#loopAborts.get(loopId)?.signal ?? signal, grammar: await this.#grammarConstraint(provider), maxTokens: this.#packets.maxTokensFor(provider), strikes: this.#strikes.streak(loopId), attributions: attributions.length > 0 ? attributions : undefined, client: client ?? undefined, workspaceId: String(workspaceId), loop: loopSeq, turn: seq }); // strikes: first-party routing signal, 0 sent explicitly (#313) // §provider-surface-generate §provider-guarantees-single-call §provider-guarantees-signal-wired §attribution-plurnk-namespace-reserved §client-telemetry
|
|
854
873
|
if (!signal?.aborted)
|
|
855
|
-
this.#telemetry.push(
|
|
874
|
+
this.#telemetry.push(workspaceId, loopId, { source: "engine:turn", kind: "turn_generated", level: "info", message: "parsing model response" });
|
|
856
875
|
}
|
|
857
876
|
catch (err) {
|
|
858
877
|
// §turn-never-blank — a ProviderError is an INFRASTRUCTURE failure (auth, network
|
|
@@ -866,7 +885,7 @@ export default class Engine {
|
|
|
866
885
|
// adjudication into a model-behavior 422 — a state the system otherwise forbids,
|
|
867
886
|
// a record that lied, and days of forensics pointed at the wrong suspect.
|
|
868
887
|
if (err instanceof ProviderError) {
|
|
869
|
-
this.#telemetry.push(
|
|
888
|
+
this.#telemetry.push(workspaceId, loopId, { source: "provider", kind: err.kind, message: err.message, level: "error" });
|
|
870
889
|
}
|
|
871
890
|
throw err;
|
|
872
891
|
}
|
|
@@ -875,18 +894,6 @@ export default class Engine {
|
|
|
875
894
|
// call-metadata (usage, finishReason, model) → Turn columns per
|
|
876
895
|
// Turn.json. Mixing the two on packet.assistant was the wrong layer.
|
|
877
896
|
const { packetAssistant, callMetadata, parseErrors } = this.#splitResponse(response); // raw assistant content is opaque — split, never interpreted — §provider-guarantees-assistantraw-opaque
|
|
878
|
-
// §tokenomics-ceiling-calibrates-to-usage — learn the loop's real/measured ratio from the
|
|
879
|
-
// provider's OWN prompt count (ground truth for the whole wire request, template overhead
|
|
880
|
-
// included). Monotone max within the loop. An EXACT ruler floors at 1 (never expands);
|
|
881
|
-
// a certified upper-bound ruler calibrates to observed truth in BOTH directions — the
|
|
882
|
-
// worst-observed packing wins, and expansion toward ground truth cannot overshoot the
|
|
883
|
-
// window (owner-ruled; run24: the unconditional floor halved gbuild's effective budget).
|
|
884
|
-
if (callMetadata.usage.prompt > 0 && requestPacket.tokens > 0) {
|
|
885
|
-
const observed = callMetadata.usage.prompt / requestPacket.tokens;
|
|
886
|
-
const prior = this.#tokenRatios.get(loopId) ?? (gauge.exact ? 1 : observed);
|
|
887
|
-
if (observed >= prior)
|
|
888
|
-
this.#tokenRatios.set(loopId, observed);
|
|
889
|
-
}
|
|
890
897
|
// Surface parse errors to the model's NEXT packet so it can self-
|
|
891
898
|
// correct. Without this, malformed emissions (e.g. a READ matcher
|
|
892
899
|
// body starting with `//` being interpreted as xpath) silently
|
|
@@ -914,7 +921,7 @@ export default class Engine {
|
|
|
914
921
|
const located = typeof event.position === "number"
|
|
915
922
|
? this.#offsetToLineColumn(packetAssistant.content, event.position)
|
|
916
923
|
: null;
|
|
917
|
-
this.#telemetry.push(
|
|
924
|
+
this.#telemetry.push(workspaceId, loopId, {
|
|
918
925
|
source: event.source,
|
|
919
926
|
kind: event.kind,
|
|
920
927
|
message: event.message ?? "",
|
|
@@ -952,8 +959,14 @@ export default class Engine {
|
|
|
952
959
|
: realOpsCount === 0 ? TURN_STATUS_NO_OPS : TURN_STATUS_IMPLICIT_CONTINUE;
|
|
953
960
|
// Idle turn: an implicit-continue (102) that did no WORK — its ops are only PLAN/SEND, no mid op.
|
|
954
961
|
// The model continued with nothing to do. (Skipped when premature already steered this turn.)
|
|
962
|
+
// #467 (owner criterion) — a turn whose op ATTEMPTS died at the parser is a FAILED-RETRIEVAL
|
|
963
|
+
// turn, not an idle one: the model performed an op; the op died. The root parse-400 speaks
|
|
964
|
+
// alone; stacking the idle-409 on it calls the turn something it factually wasn't (run65's
|
|
965
|
+
// one malformed FIND minted two error rows for one accident). A dispatched-but-failed op is
|
|
966
|
+
// already a mid op, so only the parse-emptied class needs the deference. GBNF makes the
|
|
967
|
+
// emitted-idle shape unemittable (grammar SPEC, no-idle-102); this 409 stays the truly-idle backstop.
|
|
955
968
|
const midOpsCount = packetAssistant.ops.filter((op) => op.op !== "PLAN" && op.op !== "SEND").length;
|
|
956
|
-
if (!steerStruck && turnStatus === TURN_STATUS_IMPLICIT_CONTINUE && midOpsCount === 0) {
|
|
969
|
+
if (!steerStruck && turnStatus === TURN_STATUS_IMPLICIT_CONTINUE && midOpsCount === 0 && parseErrors.length === 0) {
|
|
957
970
|
// One grace turn after a retrieval-only 409 (admins specimen): the refusal steer says
|
|
958
971
|
// "continuing in order to receive results" — a model that obediently waits one bare
|
|
959
972
|
// [102] turn is following OUR advice, and the idle rail was executing it for that.
|
|
@@ -981,7 +994,7 @@ export default class Engine {
|
|
|
981
994
|
usage_reasoning: usage.reasoning,
|
|
982
995
|
usage_cached: usage.cached,
|
|
983
996
|
usage_cost_pico: provider.costFor(usage), // §provider-surface-costfor
|
|
984
|
-
|
|
997
|
+
usage_prompt_budget: this.#packets.promptBudgetFor(provider), // #274 — the PROMPT BUDGET (window − reserves): the raw n_ctx overstated usable room by the reserve total
|
|
985
998
|
finish_reason: finishReason,
|
|
986
999
|
model,
|
|
987
1000
|
// #252 — opaque provider→client metadata passthrough (e.g. balancePico the
|
|
@@ -991,15 +1004,13 @@ export default class Engine {
|
|
|
991
1004
|
// Dispatch model ops starting at nextActionIndex (continues the
|
|
992
1005
|
// turn's running counter after any pre-model writes).
|
|
993
1006
|
//
|
|
994
|
-
// Max-commands
|
|
995
|
-
//
|
|
996
|
-
//
|
|
997
|
-
//
|
|
998
|
-
//
|
|
999
|
-
//
|
|
1000
|
-
|
|
1001
|
-
// #232 — a session's maxCommands is a tighten-only ceiling: min() the env ceiling.
|
|
1002
|
-
const maxCommands = Math.min(readMaxCommands(), (await SessionSettings.read(this.#db, sessionId)).maxCommands ?? Number.POSITIVE_INFINITY);
|
|
1007
|
+
// Max-commands ceiling: OFF by default (unlimited) — every generated op dispatches.
|
|
1008
|
+
// A degenerate op-loop is a sampler failure guarded at generation, not by dropping
|
|
1009
|
+
// already-generated work post-hoc. The ceiling is an OPT-IN operator/client bound:
|
|
1010
|
+
// when set, overflow ops drop without per-op log entries (no forensics flood) and the
|
|
1011
|
+
// model gets one telemetry signal next packet.
|
|
1012
|
+
// #232 — a workspace's maxCommands is a tighten-only ceiling: min() the env ceiling.
|
|
1013
|
+
const maxCommands = Math.min(readMaxCommands(), (await WorkspaceSettings.read(this.#db, workspaceId)).maxCommands ?? Number.POSITIVE_INFINITY);
|
|
1003
1014
|
// PLAN (reasoning) and a terminal SEND (signal ≥ 200, the conclusion) are not
|
|
1004
1015
|
// actions — they always dispatch and never count against the cap. maxCommands
|
|
1005
1016
|
// bounds real actions only; maxCommands:0 still admits a plan and a conclusion
|
|
@@ -1016,9 +1027,9 @@ export default class Engine {
|
|
|
1016
1027
|
let rowSeq = nextActionIndex;
|
|
1017
1028
|
for (const statement of opsToDispatch) {
|
|
1018
1029
|
const result = await this.dispatch({
|
|
1019
|
-
statement,
|
|
1030
|
+
statement, workspaceId, workerId, loopId, turnId,
|
|
1020
1031
|
sequence: rowSeq,
|
|
1021
|
-
origin, onDispatch,
|
|
1032
|
+
origin, onDispatch,
|
|
1022
1033
|
// §send-200-failed-ops — parse errors mint as rows AFTER this loop; the terminal
|
|
1023
1034
|
// gate needs them NOW, so the count rides the dispatch context.
|
|
1024
1035
|
turnParseErrors: parseErrors?.length ?? 0,
|
|
@@ -1057,25 +1068,33 @@ export default class Engine {
|
|
|
1057
1068
|
if (droppedCount > 0)
|
|
1058
1069
|
pendingEngineErrors.push("max_commands_exceeded");
|
|
1059
1070
|
for (const kind of pendingEngineErrors)
|
|
1060
|
-
await this.#telemetry.mintEngineError(kind, {
|
|
1071
|
+
await this.#telemetry.mintEngineError(kind, { workerId, loopId, turnId, sequence: errSeq++ });
|
|
1061
1072
|
// §log-row-self-explains (Q2, owner-clarified) — a model-op failure is the MODEL'S OWN op
|
|
1062
1073
|
// result: the op row carries its failure message on its meta line (packet-wire), and the
|
|
1063
1074
|
// errors section points at the row. No separate minted item (the retired action_failure
|
|
1064
1075
|
// mint dressed op results as source:"engine" faults — the jumbo model chased a phantom
|
|
1065
1076
|
// "engine run 400 error" off a message-less item). Genuine engine-internal faults CRASH
|
|
1066
1077
|
// (fail-hard, §turn-never-blank) and never mint model-facing rows.
|
|
1067
|
-
// §tokenomics-output-truncated
|
|
1068
|
-
//
|
|
1069
|
-
//
|
|
1070
|
-
//
|
|
1071
|
-
|
|
1078
|
+
// §tokenomics-output-truncated — packet honesty at the completion cap: a finish=length turn was
|
|
1079
|
+
// GUILLOTINED at the decode pool, and this actionless row MINTS to LEAD the artifact rows it
|
|
1080
|
+
// explains (the cause must lead, or the model fixes syntax forever). Two shapes, honestly
|
|
1081
|
+
// distinguished: content cut MID-OP (a valid prefix dispatched; the parse errors are the
|
|
1082
|
+
// severed tail) vs the pool consumed with NOTHING emitted (reasoning ran away — the parse
|
|
1083
|
+
// 'must begin with PLAN' is an artifact of the empty emission, not a malformed turn). The parse
|
|
1084
|
+
// rows below STAY (the record never hides); the 413 leads and frames them for what they are.
|
|
1085
|
+
const truncatedEmpty = finishReason === "length" && packetAssistant.content.trim().length === 0;
|
|
1086
|
+
if (finishReason === "length" && (truncatedEmpty || (parseErrors?.length ?? 0) > 0)) {
|
|
1087
|
+
const cap = this.#packets.maxTokensFor(provider);
|
|
1088
|
+
const message = truncatedEmpty
|
|
1089
|
+
? `output truncated at the completion cap (${cap} tokens): nothing was emitted before the pool was consumed — the parse error below is an artifact of the empty emission, not a malformed turn`
|
|
1090
|
+
: `output truncated at the completion cap (${cap} tokens): the emission was cut mid-op — the parse errors below are truncation artifacts`;
|
|
1072
1091
|
await this.#db.engine_insert_log_entry.get({
|
|
1073
|
-
|
|
1092
|
+
worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence: errSeq++,
|
|
1074
1093
|
origin: "model", source: "engine", op: "error", suffix: "", signal: null,
|
|
1075
1094
|
scheme: null, username: null, password: null, hostname: null, port: null,
|
|
1076
1095
|
pathname: null, params: null, fragment: null, lineMarker: null,
|
|
1077
1096
|
tx: "", mimetype_tx: "text/plain",
|
|
1078
|
-
rx: JSON.stringify({ status: 413, kind: "output_truncated", message
|
|
1097
|
+
rx: JSON.stringify({ status: 413, kind: "output_truncated", message }),
|
|
1079
1098
|
mimetype_rx: "application/json", status_rx: 413, tokens: 0, state: "failed", outcome: "output_truncated",
|
|
1080
1099
|
attrs: "{}",
|
|
1081
1100
|
});
|
|
@@ -1084,7 +1103,7 @@ export default class Engine {
|
|
|
1084
1103
|
// resolved against the model's folded mirror row (§model-entry) — origin 'model', not engine.
|
|
1085
1104
|
for (const { message, line, column, source } of parseErrors ?? []) {
|
|
1086
1105
|
await this.#db.engine_insert_log_entry.get({
|
|
1087
|
-
|
|
1106
|
+
worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence: errSeq++,
|
|
1088
1107
|
origin: "model", source: "grammar", op: "error", suffix: "", signal: null,
|
|
1089
1108
|
scheme: null, username: null, password: null, hostname: null, port: null,
|
|
1090
1109
|
pathname: null, params: null, fragment: null, lineMarker: null,
|
|
@@ -1106,7 +1125,7 @@ export default class Engine {
|
|
|
1106
1125
|
// introspect any prior emission of its own the same way. Empty emissions (a struck/
|
|
1107
1126
|
// silent turn) write nothing — no prior output to mirror.
|
|
1108
1127
|
if (packetAssistant.content.trim().length > 0) {
|
|
1109
|
-
await this.#dispatcher.writeModelEntry({ verbatim: packetAssistant.content,
|
|
1128
|
+
await this.#dispatcher.writeModelEntry({ verbatim: packetAssistant.content, workerId, loopId, turnId, sequence: errSeq++, folded: true });
|
|
1110
1129
|
}
|
|
1111
1130
|
// Zero ops is NOT an error to report — the model knows it emitted
|
|
1112
1131
|
// nothing. Strike accounting (engine-internal) treats it as a
|
|
@@ -1180,40 +1199,40 @@ export default class Engine {
|
|
|
1180
1199
|
}
|
|
1181
1200
|
// #note12 — the daughter-provided reference docs (schemes' + execs' `documentation`),
|
|
1182
1201
|
// materialized at plurnk:///docs/<name>.md by loop_run (like operator docs).
|
|
1183
|
-
docEntries(
|
|
1184
|
-
return this.#packets.docEntries(
|
|
1202
|
+
docEntries(workspaceId) {
|
|
1203
|
+
return this.#packets.docEntries(workspaceId);
|
|
1185
1204
|
}
|
|
1186
1205
|
// §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
|
|
1187
|
-
// run last looked. No per-
|
|
1206
|
+
// run last looked. No per-worker snapshot (§machine-processes "a worker is its log"): every
|
|
1188
1207
|
// edit is already a span-carrying log row, so PULL other actors' EDITs on shared
|
|
1189
|
-
// entries since this
|
|
1208
|
+
// entries since this worker's prior turn — real cross-worker edits and the plurnk worker's
|
|
1190
1209
|
// fs-sync fictions — and materialize each as a FOLDED delta reusing the row's span +
|
|
1191
1210
|
// cause. Returns the count so the caller advances nextActionIndex past the deltas.
|
|
1192
1211
|
async #materializeEnvironmentDeltas(args) {
|
|
1193
|
-
const {
|
|
1194
|
-
const boundary = await this.#db.
|
|
1212
|
+
const { workspaceId, workerId, loopId, turnId, fromSequence } = args;
|
|
1213
|
+
const boundary = await this.#db.engine_worker_prior_turn_time.get({ worker_id: workerId, turn_id: turnId });
|
|
1195
1214
|
const since = boundary?.since ?? null;
|
|
1196
1215
|
if (since === null)
|
|
1197
1216
|
return 0; // first turn — nothing prior; the model reads current state fresh
|
|
1198
|
-
const rows = await this.#db.engine_pull_env_deltas.all({
|
|
1217
|
+
const rows = await this.#db.engine_pull_env_deltas.all({ workspace_id: workspaceId, worker_id: workerId, since });
|
|
1199
1218
|
let written = 0;
|
|
1200
1219
|
for (const r of rows) {
|
|
1201
|
-
// source: the originating run (a real cross-
|
|
1220
|
+
// source: the originating run (a real cross-worker edit) or 'file' (an fs fiction);
|
|
1202
1221
|
// rx reuses the originating row's result span — the edit as it looked then.
|
|
1203
1222
|
await this.#db.engine_insert_env_delta.run({
|
|
1204
|
-
|
|
1205
|
-
source: r.source ?? String(r.
|
|
1223
|
+
worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence: fromSequence + written,
|
|
1224
|
+
source: r.source ?? String(r.worker_id), scheme: r.scheme, pathname: r.pathname, rx: r.rx,
|
|
1206
1225
|
});
|
|
1207
1226
|
written++;
|
|
1208
1227
|
}
|
|
1209
|
-
// §
|
|
1228
|
+
// §worker-scheme — loop-terminations: a sibling's loop reaching terminal surfaces the
|
|
1210
1229
|
// same way an entry-change does, carrying its deliverable (the SEND body) or the
|
|
1211
1230
|
// abandonment reason. Folded, attributed to the terminated run.
|
|
1212
|
-
const terms = await this.#db.engine_pull_loop_terminations.all({
|
|
1231
|
+
const terms = await this.#db.engine_pull_loop_terminations.all({ workspace_id: workspaceId, worker_id: workerId, since });
|
|
1213
1232
|
for (const t of terms) {
|
|
1214
1233
|
await this.#db.engine_insert_loop_termination_delta.run({
|
|
1215
|
-
|
|
1216
|
-
source: String(t.
|
|
1234
|
+
worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence: fromSequence + written,
|
|
1235
|
+
source: String(t.worker_id), pathname: `/${t.worker_name}`,
|
|
1217
1236
|
rx: markTerminal(t.terminated_by, t.terminal_message) ?? `loop "${t.prompt}" ended (${t.status})`,
|
|
1218
1237
|
status: t.status,
|
|
1219
1238
|
});
|
|
@@ -1221,13 +1240,13 @@ export default class Engine {
|
|
|
1221
1240
|
}
|
|
1222
1241
|
return written;
|
|
1223
1242
|
}
|
|
1224
|
-
// §exec-poll — EXEC `<0>` is turn-scoped: abort the
|
|
1243
|
+
// §exec-poll — EXEC `<0>` is turn-scoped: abort the worker's open turn-scoped streams via their
|
|
1225
1244
|
// owning scheme (the same registry-routed abort the total reap uses). Called at each pre-turn
|
|
1226
1245
|
// before the turn's own spawns, so every open turn-scoped sub here is from a prior turn — it
|
|
1227
1246
|
// never survives into the subsequent turn. Fire-and-forget: the spawn finalizes async and its
|
|
1228
1247
|
// terminal output surfaces born-OPEN through the stream-delta path (§exec-stream).
|
|
1229
|
-
async #reapTurnScopedStreams(
|
|
1230
|
-
const open = await this.#db.
|
|
1248
|
+
async #reapTurnScopedStreams(workerId) {
|
|
1249
|
+
const open = await this.#db.find_open_turn_scoped_subscriptions_for_worker.all({ worker_id: workerId });
|
|
1231
1250
|
for (const { id, scheme } of open) {
|
|
1232
1251
|
const handler = this.#schemes.get(scheme);
|
|
1233
1252
|
handler?.abortSubscription?.(id);
|
|
@@ -1239,12 +1258,12 @@ export default class Engine {
|
|
|
1239
1258
|
// streamEnd recorded on the channel's prior delta — no exec-specific surfacing, just the
|
|
1240
1259
|
// env-observe loop with a byte cursor where env-delta uses a timestamp. §exec-stream
|
|
1241
1260
|
async #materializeStreamDeltas(args) {
|
|
1242
|
-
const {
|
|
1243
|
-
const channels = await this.#db.
|
|
1261
|
+
const { workerId, loopId, turnId, fromSequence } = args;
|
|
1262
|
+
const channels = await this.#db.engine_worker_stream_channels.all({ worker_id: workerId });
|
|
1244
1263
|
let written = 0;
|
|
1245
1264
|
for (const ch of channels) {
|
|
1246
1265
|
const prior = await this.#db.engine_stream_cursor.get({
|
|
1247
|
-
|
|
1266
|
+
worker_id: workerId, scheme: ch.runtime, pathname: ch.coord, fragment: ch.channel,
|
|
1248
1267
|
});
|
|
1249
1268
|
const priorAttrs = prior !== undefined ? JSON.parse(prior.attrs) : {};
|
|
1250
1269
|
const cursor = priorAttrs.streamEnd ?? 0;
|
|
@@ -1258,7 +1277,7 @@ export default class Engine {
|
|
|
1258
1277
|
// pointer to the already-delivered bytes, never a re-send (§tokenomics-fetch-fits-free).
|
|
1259
1278
|
if (closed && priorAttrs.terminal !== true && cursor > 0) {
|
|
1260
1279
|
await this.#db.engine_insert_stream_delta.run({
|
|
1261
|
-
|
|
1280
|
+
worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence: fromSequence + written,
|
|
1262
1281
|
scheme: ch.runtime, pathname: ch.coord, fragment: ch.channel,
|
|
1263
1282
|
rx: JSON.stringify({ status: ch.close_status ?? 200, content: `[ stream closed (${ch.close_status ?? 200}) — full output already delivered above; READ ${ch.runtime}://${ch.coord}#${ch.channel} to revisit ]`, mimetype: "text/stream" }),
|
|
1264
1283
|
attrs: JSON.stringify({ streamEnd: ch.content.length, terminal: true }),
|
|
@@ -1272,7 +1291,7 @@ export default class Engine {
|
|
|
1272
1291
|
// into one sequence (lines N..M, then M+1..), not N independent "1:" restarts. §exec-stream
|
|
1273
1292
|
const startLine = (ch.content.slice(0, cursor).match(/\n/g)?.length ?? 0) + 1;
|
|
1274
1293
|
await this.#db.engine_insert_stream_delta.run({
|
|
1275
|
-
|
|
1294
|
+
worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence: fromSequence + written,
|
|
1276
1295
|
scheme: ch.runtime, pathname: ch.coord, fragment: ch.channel,
|
|
1277
1296
|
rx: JSON.stringify({ status: 200, content: ch.content.slice(cursor), mimetype: "text/stream", startLine }),
|
|
1278
1297
|
attrs: JSON.stringify({ streamEnd: ch.content.length, terminal: closed }),
|
|
@@ -1283,20 +1302,20 @@ export default class Engine {
|
|
|
1283
1302
|
return written;
|
|
1284
1303
|
}
|
|
1285
1304
|
// §env-delta — the filesystem as an actor. Ambient disk divergences detected at
|
|
1286
|
-
// pre-turn (git membership re-read) are logged as the plurnk
|
|
1305
|
+
// pre-turn (git membership re-read) are logged as the plurnk worker's source=file EDIT
|
|
1287
1306
|
// "fictions": no op happened, but EDIT is the only grammar the model has for "your
|
|
1288
1307
|
// world changed," so the fiction keeps its perspective aligned with what its tooling
|
|
1289
|
-
// would show. The fiction lives in the plurnk
|
|
1308
|
+
// would show. The fiction lives in the plurnk worker's log; every other run pulls it
|
|
1290
1309
|
// through the one delta path, exactly like a sibling's real edit.
|
|
1291
|
-
// §membership-emi-divergence-signal — disk divergences logged as the plurnk
|
|
1292
|
-
async #logFsFictions(
|
|
1310
|
+
// §membership-emi-divergence-signal — disk divergences logged as the plurnk worker's source=file EDIT fictions
|
|
1311
|
+
async #logFsFictions(workspaceId, divergences) {
|
|
1293
1312
|
if (divergences.length === 0)
|
|
1294
1313
|
return;
|
|
1295
|
-
const run = await this.#db.
|
|
1296
|
-
?? await this.#db.
|
|
1314
|
+
const run = await this.#db.envelope_get_worker_by_name.get({ workspace_id: workspaceId, name: "plurnk" })
|
|
1315
|
+
?? await this.#db.envelope_insert_worker.get({ workspace_id: workspaceId, name: "plurnk", origin: "plurnk" });
|
|
1297
1316
|
if (run === undefined)
|
|
1298
|
-
throw new Error("logFsFictions: plurnk
|
|
1299
|
-
const loop = await this.#db.envelope_insert_client_loop.get({
|
|
1317
|
+
throw new Error("logFsFictions: plurnk worker resolution returned no row");
|
|
1318
|
+
const loop = await this.#db.envelope_insert_client_loop.get({ worker_id: run.id });
|
|
1300
1319
|
if (loop === undefined)
|
|
1301
1320
|
throw new Error("logFsFictions: loop insert returned no row");
|
|
1302
1321
|
const seq = await this.#db.client_turn_next_sequence.get({ loop_id: loop.id });
|
|
@@ -1307,7 +1326,7 @@ export default class Engine {
|
|
|
1307
1326
|
for (const d of divergences) {
|
|
1308
1327
|
const span = editedSpan(d.before, d.after);
|
|
1309
1328
|
await this.#db.engine_insert_log_entry.get({
|
|
1310
|
-
|
|
1329
|
+
worker_id: run.id, loop_id: loop.id, turn_id: turn.id, sequence: sequence++,
|
|
1311
1330
|
origin: "plurnk", source: "file", op: "EDIT", suffix: "", signal: null,
|
|
1312
1331
|
scheme: d.scheme, username: null, password: null, hostname: null, port: null,
|
|
1313
1332
|
pathname: d.pathname, params: null, fragment: null, lineMarker: null,
|
|
@@ -1349,79 +1368,63 @@ export default class Engine {
|
|
|
1349
1368
|
// run still accepting turns?" If yes, skip the wake — the active loop
|
|
1350
1369
|
// will pick up the channel transition at its next turn boundary. If no,
|
|
1351
1370
|
// the daemon opens a fresh loop with the wake prompt.
|
|
1352
|
-
async
|
|
1353
|
-
const row = await this.#db.
|
|
1371
|
+
async hasActiveLoopForWorker(workerId) {
|
|
1372
|
+
const row = await this.#db.engine_count_active_loops_for_worker.get({ worker_id: workerId });
|
|
1354
1373
|
return (row?.n ?? 0) > 0;
|
|
1355
1374
|
}
|
|
1356
1375
|
// #290 — run the derivation pump (deep channels: symbols/refs/FTS +
|
|
1357
1376
|
// embeddings, deep_hash-gated) at SESSION-SCOPE, off the per-turn path, so a freshly-created
|
|
1358
|
-
//
|
|
1359
|
-
// loop.run.
|
|
1377
|
+
// workspace's corpus warms DURING the client's startup window instead of freezing the first
|
|
1378
|
+
// loop.run. workspace.create fires this and returns immediately; embed_progress live-fans-out as it
|
|
1360
1379
|
// runs. Idempotent + deep_hash-gated, so turn 1's pump finds the work done (or harmlessly re-runs);
|
|
1361
1380
|
// a no-embedder build derives the cheap symbols/refs/FTS channels and skips the embed pass. Has no
|
|
1362
1381
|
// loop yet — telemetry fans out live only (loopId 0), never buffered to a loop that never drains.
|
|
1363
|
-
async
|
|
1382
|
+
async warmWorkspaceDerivations(workspaceId) {
|
|
1364
1383
|
const ctx = {
|
|
1365
|
-
db: this.#db,
|
|
1384
|
+
db: this.#db, workspaceId, workerId: 0, loopId: 0, turnId: 0,
|
|
1366
1385
|
writer: "plurnk",
|
|
1367
1386
|
signal: undefined,
|
|
1368
1387
|
streamEventNotify: this.#streamEventNotify,
|
|
1369
|
-
|
|
1388
|
+
wakeWorkerNotify: this.#wakeWorkerNotify,
|
|
1370
1389
|
tokenize: this.#tokenize,
|
|
1371
1390
|
mimetypes: this.#mimetypes,
|
|
1372
1391
|
defaultChannelFor: (s) => this.#schemes.defaultChannelFor(s),
|
|
1373
|
-
pushTelemetry: (event) => this.#telemetry.notify(
|
|
1392
|
+
pushTelemetry: (event) => this.#telemetry.notify(workspaceId, 0, event),
|
|
1374
1393
|
};
|
|
1375
1394
|
await this.#queueDerivation(() => EntryManifest.maintainDerivations(ctx)); // §derivation-off-hot-path
|
|
1376
1395
|
}
|
|
1377
|
-
// Inject a prompt into the
|
|
1396
|
+
// Inject a prompt into the worker's currently-executing loop. Writes a
|
|
1378
1397
|
// plurnk://prompt/<run>/<loop>/<next-turn> entry whose body becomes the
|
|
1379
1398
|
// prompt section at the next turn boundary. Last-wins: if two
|
|
1380
1399
|
// injects target the same next-turn slot, the second overwrites the
|
|
1381
1400
|
// first.
|
|
1382
1401
|
//
|
|
1383
|
-
// Returns null when no loop in the
|
|
1402
|
+
// Returns null when no loop in the worker is currently active (status=102).
|
|
1384
1403
|
// The daemon-side inject path then enqueues a fresh loop with this
|
|
1385
1404
|
// prompt; engine doesn't open loops itself.
|
|
1386
1405
|
//
|
|
1387
1406
|
// Rummy parallel: AgentLoop.inject(). The "active drain → write
|
|
1388
1407
|
// prompt entry, return immediately" branch.
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
async prefetchCtx(sessionId, runId) {
|
|
1392
|
-
const loop = await this.#db.drain_current_loop_for_run.get({ run_id: runId });
|
|
1393
|
-
const loopId = loop?.id ?? 0;
|
|
1394
|
-
return {
|
|
1395
|
-
db: this.#db, sessionId, runId, loopId, turnId: 0,
|
|
1396
|
-
writer: "plurnk",
|
|
1397
|
-
signal: this.#loopAborts.get(loopId)?.signal,
|
|
1398
|
-
streamEventNotify: this.#streamEventNotify,
|
|
1399
|
-
wakeRunNotify: this.#wakeRunNotify,
|
|
1400
|
-
tokenize: this.#tokenize,
|
|
1401
|
-
mimetypes: this.#mimetypes,
|
|
1402
|
-
pushTelemetry: (event) => this.#telemetry.push(sessionId, loopId, event),
|
|
1403
|
-
};
|
|
1404
|
-
}
|
|
1405
|
-
async inject(runId, prompt) {
|
|
1406
|
-
const loopRow = await this.#db.drain_current_loop_for_run.get({ run_id: runId });
|
|
1408
|
+
async inject(workerId, prompt) {
|
|
1409
|
+
const loopRow = await this.#db.drain_current_loop_for_worker.get({ worker_id: workerId });
|
|
1407
1410
|
if (loopRow === undefined)
|
|
1408
1411
|
return null;
|
|
1409
1412
|
const loopId = loopRow.id;
|
|
1410
1413
|
const turnRow = await this.#db.drain_next_turn_seq_for_loop.get({ loop_id: loopId });
|
|
1411
1414
|
const turnSeq = turnRow?.next ?? 1;
|
|
1412
|
-
const
|
|
1413
|
-
if (
|
|
1414
|
-
throw new Error(`Engine.inject: run ${
|
|
1415
|
-
const pathname = promptPathname(
|
|
1415
|
+
const workspaceRow = await this.#db.drain_get_worker_workspace.get({ worker_id: workerId });
|
|
1416
|
+
if (workspaceRow === undefined)
|
|
1417
|
+
throw new Error(`Engine.inject: run ${workerId} not found`);
|
|
1418
|
+
const pathname = promptPathname(workerId, loopRow.sequence, turnSeq); // canonical storage form, worker-qualified loop-SEQ coordinates matching the turn-1 foist
|
|
1416
1419
|
const ctx = {
|
|
1417
|
-
db: this.#db,
|
|
1420
|
+
db: this.#db, workspaceId: workspaceRow.workspace_id, workerId, loopId,
|
|
1418
1421
|
turnId: 0, // no turn open at inject time; entries don't pin turnId
|
|
1419
1422
|
writer: "plurnk",
|
|
1420
1423
|
signal: this.#loopAborts.get(loopId)?.signal,
|
|
1421
1424
|
streamEventNotify: this.#streamEventNotify,
|
|
1422
|
-
|
|
1425
|
+
wakeWorkerNotify: this.#wakeWorkerNotify,
|
|
1423
1426
|
tokenize: this.#tokenize,
|
|
1424
|
-
pushTelemetry: (event) => this.#telemetry.push(
|
|
1427
|
+
pushTelemetry: (event) => this.#telemetry.push(workspaceRow.workspace_id, loopId, event),
|
|
1425
1428
|
};
|
|
1426
1429
|
const entry = {
|
|
1427
1430
|
channels: { body: { content: prompt, mimetype: "text/markdown" } },
|