@plurnk/plurnk-service 1.0.5 → 1.0.7
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 +66 -48
- package/INSTALL.md +3 -3
- package/README.md +4 -4
- package/SPEC.md +212 -196
- 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 +15 -11
- package/dist/core/Dispatcher.d.ts.map +1 -1
- package/dist/core/Dispatcher.js +124 -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 +210 -185
- package/dist/core/Engine.js.map +1 -1
- package/dist/core/Engine.sql +106 -109
- package/dist/core/PacketBuilder.d.ts +9 -9
- package/dist/core/PacketBuilder.d.ts.map +1 -1
- package/dist/core/PacketBuilder.js +199 -151
- 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/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 -6
- 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.js +2 -2
- package/dist/core/token-ruler.js.map +1 -1
- 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 +83 -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 +124 -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 +8 -8
- 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 +72 -28
- 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 +48 -48
- package/dist/server/Daemon.d.ts.map +1 -1
- package/dist/server/Daemon.js +295 -292
- 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/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 +22 -7
- package/dist/service.js.map +1 -1
- package/migrations/0000-00-00.01_schema.sql +82 -70
- package/package.json +18 -15
- 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/
|
|
5
|
+
import { markTerminal } from "../schemes/Worker.js";
|
|
6
6
|
import GitMembership from "./git-membership.js";
|
|
7
7
|
import GitState from "./git-state.js";
|
|
8
|
-
import
|
|
8
|
+
import WorkspaceSettings from "./workspace-settings.js";
|
|
9
9
|
import { editedSpan } from "../content/index.js";
|
|
10
10
|
import { promptPathname } from "./plurnk-uri.js";
|
|
11
11
|
import { rulerCount } from "./token-ruler.js";
|
|
12
|
-
import SearchPrefetch from "./search-prefetch.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;
|
|
@@ -142,11 +143,11 @@ export default class Engine {
|
|
|
142
143
|
// (the steer's own advice is to wait; in-memory, fail-open on restart).
|
|
143
144
|
#retrievalRefusalGrace = new Set();
|
|
144
145
|
// §derivation-off-hot-path — the background derivation chain: the per-turn pump and the
|
|
145
|
-
//
|
|
146
|
+
// workspace warm ride it instead of the turn (a 2-CPU container CPU-embedding a 335-entry
|
|
146
147
|
// ingest starved every loop for ~28min, #316). Serialized (never two pumps interleaved),
|
|
147
148
|
// drained at daemon stop (never racing db close), failures logged (never swallowed).
|
|
148
149
|
// A turn never waits on an embedding; a ~query warms its own candidate slice inline
|
|
149
|
-
// (§semantic-cold-query-full-fidelity), so cold
|
|
150
|
+
// (§semantic-cold-query-full-fidelity), so cold workspaces still get full-fidelity search.
|
|
150
151
|
#derivationChain = Promise.resolve();
|
|
151
152
|
#queueDerivation(job) {
|
|
152
153
|
const run = this.#derivationChain.then(job).catch((err) => {
|
|
@@ -160,14 +161,15 @@ export default class Engine {
|
|
|
160
161
|
await this.#derivationChain;
|
|
161
162
|
}
|
|
162
163
|
#streamEventNotify;
|
|
163
|
-
#
|
|
164
|
+
#wakeWorkerNotify;
|
|
164
165
|
// Cached plurnk GBNF — read once on the first constrained generate (#189).
|
|
165
166
|
#gbnfCache = new Map(); // variant name -> GBNF text (per-alias selection, #353)
|
|
166
|
-
|
|
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 }) {
|
|
167
169
|
this.#db = db;
|
|
168
170
|
this.#schemes = schemes;
|
|
169
171
|
this.#streamEventNotify = streamEventNotify;
|
|
170
|
-
this.#
|
|
172
|
+
this.#wakeWorkerNotify = wakeWorkerNotify;
|
|
171
173
|
// Default to empty discovery — standalone Engine construction (in
|
|
172
174
|
// tests) gets no handlers, and content flows through the framework's
|
|
173
175
|
// raw-content fitContent fallback. Daemon-managed Engine receives a
|
|
@@ -184,12 +186,9 @@ export default class Engine {
|
|
|
184
186
|
this.#telemetry = new TelemetryChannel({ db, notify: telemetryEventNotify });
|
|
185
187
|
this.#strikes = new StrikeRail();
|
|
186
188
|
this.#packets = new PacketBuilder({ db, schemes, telemetry: this.#telemetry, executors });
|
|
187
|
-
// §search-prefetch — the web-member sync pass; the daemon's wake handler drives it,
|
|
188
|
-
// the hold loop below honors it (holds() = a pass in flight for the run).
|
|
189
|
-
this.searchPrefetch = new SearchPrefetch({ db: this.#db, schemes: this.#schemes, tokenize: this.#tokenize });
|
|
190
189
|
this.#proposals = new ProposalLifecycle({
|
|
191
190
|
db, schemes, telemetry: this.#telemetry,
|
|
192
|
-
streamEventNotify,
|
|
191
|
+
streamEventNotify, wakeWorkerNotify,
|
|
193
192
|
tokenize: this.#tokenize, mimetypes: this.#mimetypes, executors, loopSignal,
|
|
194
193
|
});
|
|
195
194
|
this.#dispatcher = new Dispatcher({ searchGate: this.searchGate,
|
|
@@ -197,7 +196,7 @@ export default class Engine {
|
|
|
197
196
|
tokenize: this.#tokenize,
|
|
198
197
|
telemetry: this.#telemetry, proposals: this.#proposals,
|
|
199
198
|
executors, loopSignal,
|
|
200
|
-
streamEventNotify,
|
|
199
|
+
streamEventNotify, wakeWorkerNotify, injectWorker, cancelWorker,
|
|
201
200
|
parkDeadlines: this.parkDeadlines,
|
|
202
201
|
joinTargets: this.joinTargets,
|
|
203
202
|
});
|
|
@@ -238,18 +237,36 @@ export default class Engine {
|
|
|
238
237
|
// that constrain sampling (llama-server, response_format backends); a cloud model that IGNORES the grammar
|
|
239
238
|
// gets a filter-mode divergence event every turn for nothing. So the bare default is OFF
|
|
240
239
|
// and the GBNF-capable aliases opt IN via a PLURNK_PROVIDERS_GBNF_<alias> suffix.
|
|
241
|
-
|
|
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 ?? "";
|
|
242
253
|
const variant = scopeEnvToAlias(process.env, alias, ["PLURNK_PROVIDERS_GBNF"]).PLURNK_PROVIDERS_GBNF;
|
|
243
|
-
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
|
+
}
|
|
244
259
|
return undefined;
|
|
260
|
+
}
|
|
245
261
|
const hit = this.#gbnfCache.get(variant);
|
|
246
262
|
if (hit !== undefined)
|
|
247
263
|
return hit;
|
|
248
264
|
const path = variant.startsWith("/") || variant.startsWith(".")
|
|
249
265
|
? variant
|
|
250
266
|
: fileURLToPath(import.meta.resolve(`@plurnk/plurnk-grammar/${variant}`));
|
|
251
|
-
const text = await readFile(path, "utf8");
|
|
267
|
+
const text = await readFile(path, "utf8"); // unresolvable/unreadable throws — a configured rail never silently degrades
|
|
252
268
|
this.#gbnfCache.set(variant, text);
|
|
269
|
+
process.stderr.write(`plurnk-engine: grammar rail: ${alias || "(bare)"} → ${variant} (${text.length} chars)\n`);
|
|
253
270
|
return text;
|
|
254
271
|
}
|
|
255
272
|
// Per-loop usage totals (#197): SUM the loop's turns (usage is stored per
|
|
@@ -272,7 +289,7 @@ export default class Engine {
|
|
|
272
289
|
// summed promptTokens above, which overcounts a context that grows across turns.
|
|
273
290
|
contextTokens: row?.context ?? 0,
|
|
274
291
|
// #274 — the last turn's model window (denominator); null when the provider reports none.
|
|
275
|
-
|
|
292
|
+
promptBudget: row?.context_size ?? null,
|
|
276
293
|
// #252 — the latest turn's opaque provider blob, parsed for the wire. Empty {} when the
|
|
277
294
|
// provider returned no meta. The service forwards it; it never reads a field within.
|
|
278
295
|
meta: JSON.parse(row?.meta ?? "{}"),
|
|
@@ -298,7 +315,7 @@ export default class Engine {
|
|
|
298
315
|
}
|
|
299
316
|
return { line, column };
|
|
300
317
|
}
|
|
301
|
-
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, }) {
|
|
302
319
|
const turnIds = [];
|
|
303
320
|
const suddenDeathThreshold = maxTurns - maxStrikes;
|
|
304
321
|
// Per-loop AbortController for scheme-side cancellation propagation.
|
|
@@ -352,7 +369,7 @@ export default class Engine {
|
|
|
352
369
|
if (row.status === 100) {
|
|
353
370
|
// NOT a terminal — a wake re-queued this loop while its own live drain was
|
|
354
371
|
// between turns (a child concluded in the gap between our 202 write and this
|
|
355
|
-
// check, §
|
|
372
|
+
// check, §worker-lifecycle-wake-requeue-not-terminal). The wake's intent is KEEP
|
|
356
373
|
// RUNNING: re-claim atomically and continue — the injected prompt is already
|
|
357
374
|
// this loop's next turn. Returning it as "external" broadcast a QUEUED loop
|
|
358
375
|
// as loop/terminated {finalStatus: 100} — the delegation-flags flake.
|
|
@@ -390,19 +407,19 @@ export default class Engine {
|
|
|
390
407
|
const holdCapMs = Number(process.env.PLURNK_SERVICE_EXEC_HOLD_MS ?? "300000");
|
|
391
408
|
if (holdSet.size > 0 && holdCapMs > 0 && execHandler?.hasActiveHoldSpawns !== undefined) {
|
|
392
409
|
const holdStart = Date.now();
|
|
393
|
-
while (
|
|
410
|
+
while (execHandler.hasActiveHoldSpawns(workerId, holdSet) && Date.now() - holdStart < holdCapMs) {
|
|
394
411
|
await delay(150, undefined, { signal });
|
|
395
412
|
}
|
|
396
413
|
}
|
|
397
414
|
const execWaitMs = Number(process.env.PLURNK_SERVICE_EXEC_WAIT_MS ?? "0");
|
|
398
415
|
if (execWaitMs > 0) {
|
|
399
|
-
if (execHandler?.hasActiveSpawns?.(
|
|
416
|
+
if (execHandler?.hasActiveSpawns?.(workerId) === true)
|
|
400
417
|
await delay(execWaitMs, undefined, { signal });
|
|
401
418
|
}
|
|
402
419
|
let turn;
|
|
403
420
|
try {
|
|
404
421
|
turn = await this.runTurn({
|
|
405
|
-
provider, messages, requirements,
|
|
422
|
+
provider, messages, requirements, workspaceId, workerId, loopId, origin, signal, onDispatch,
|
|
406
423
|
turnNumber: turnIds.length + 1, maxTurns,
|
|
407
424
|
});
|
|
408
425
|
}
|
|
@@ -449,7 +466,7 @@ export default class Engine {
|
|
|
449
466
|
}
|
|
450
467
|
}
|
|
451
468
|
}
|
|
452
|
-
async runTurn({ provider, messages, requirements = "",
|
|
469
|
+
async runTurn({ provider, messages, requirements = "", workspaceId, workerId, loopId, origin = "model", signal, onDispatch, turnNumber = 1, maxTurns = 50, }) {
|
|
453
470
|
// === Turn-as-container model ===
|
|
454
471
|
//
|
|
455
472
|
// Turn rows are created at runTurn OPEN (status=102, placeholder
|
|
@@ -462,8 +479,8 @@ export default class Engine {
|
|
|
462
479
|
// writes consume low indices; model ops continue from there.
|
|
463
480
|
const seqRow = await this.#db.engine_next_turn_sequence.get({ loop_id: loopId });
|
|
464
481
|
const seq = seqRow.next;
|
|
465
|
-
// #269 — loops.sequence is the loop's ordinal WITHIN the
|
|
466
|
-
// 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
|
|
467
484
|
// first turn; per-loop foists (the prompt, @file) still fire each loop. Read once, turn-1 only.
|
|
468
485
|
const loopRow = seq === 1
|
|
469
486
|
? await this.#db.engine_get_loop_prompt.get({ loop_id: loopId })
|
|
@@ -487,7 +504,7 @@ export default class Engine {
|
|
|
487
504
|
// from sequence=2 onward on prompt-foisted turns; 1 onward
|
|
488
505
|
// otherwise.
|
|
489
506
|
let nextActionIndex = 1;
|
|
490
|
-
// §model-entry — the
|
|
507
|
+
// §model-entry — the worker's first turn opens with the model's own turn-0, mirrored OPEN: a
|
|
491
508
|
// worked turn PLAN → the environment FINDs the foist ACTUALLY dispatches → SEND[102]. Built
|
|
492
509
|
// from the real ops below (not a static print — we lean into the genuine echo paradigm) and
|
|
493
510
|
// written at sequence 1, so it reads first as the emission with the foisted results following.
|
|
@@ -496,15 +513,15 @@ export default class Engine {
|
|
|
496
513
|
if (runFirstLoop)
|
|
497
514
|
nextActionIndex = 2; // reserve sequence 1 for the turn-0 echo
|
|
498
515
|
// Operator doc READs (PLURNK_SERVICE_MD_<ALIAS>, §actor-boundary-doc-injection). The docs were materialized
|
|
499
|
-
// as plurnk:///<entry> entries by the plurnk
|
|
516
|
+
// as plurnk:///<entry> entries by the plurnk worker (loop_run, via the
|
|
500
517
|
// §actor-boundary keystone); foist a READ of each into THIS turn-0 so the model
|
|
501
518
|
// reads them inline. It sees only the READ — the materializing EDIT
|
|
502
|
-
// lives in the plurnk
|
|
503
|
-
// #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
|
|
504
521
|
// each materialized plurnk:///<alias>.md (loop_run materialized the same set).
|
|
505
|
-
const { mdDocs } = await
|
|
506
|
-
// #269 — operator docs are run-once; foist them only on the
|
|
507
|
-
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) : []) {
|
|
508
525
|
const docTarget = {
|
|
509
526
|
kind: "url", raw: `plurnk:///${doc.entryName}`, scheme: "plurnk",
|
|
510
527
|
username: null, password: null, hostname: null, port: null,
|
|
@@ -515,7 +532,7 @@ export default class Engine {
|
|
|
515
532
|
lineMarker: null, body: null, position: { line: 1, column: 1 },
|
|
516
533
|
};
|
|
517
534
|
await this.dispatch({
|
|
518
|
-
statement: docRead,
|
|
535
|
+
statement: docRead, workspaceId, workerId, loopId, turnId,
|
|
519
536
|
sequence: nextActionIndex, origin: "plurnk", onDispatch,
|
|
520
537
|
});
|
|
521
538
|
nextActionIndex++;
|
|
@@ -523,7 +540,7 @@ export default class Engine {
|
|
|
523
540
|
const promptRow = loopRow; // #269 — already read above (per-loop; fires every loop's turn 1)
|
|
524
541
|
if (promptRow !== undefined && typeof promptRow.prompt === "string" && promptRow.prompt.length > 0) {
|
|
525
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)
|
|
526
|
-
const promptPath = promptTarget(
|
|
543
|
+
const promptPath = promptTarget(workerId, promptLoopSeq, seq);
|
|
527
544
|
const promptStmt = {
|
|
528
545
|
op: "EDIT", suffix: "", signal: null,
|
|
529
546
|
target: promptPath, lineMarker: null,
|
|
@@ -531,7 +548,7 @@ export default class Engine {
|
|
|
531
548
|
};
|
|
532
549
|
let promptLogId;
|
|
533
550
|
await this.dispatch({
|
|
534
|
-
statement: promptStmt,
|
|
551
|
+
statement: promptStmt, workspaceId, workerId, loopId, turnId,
|
|
535
552
|
sequence: nextActionIndex, origin: "plurnk",
|
|
536
553
|
onDispatch: (id) => { promptLogId = id; onDispatch?.(id); },
|
|
537
554
|
});
|
|
@@ -545,14 +562,19 @@ export default class Engine {
|
|
|
545
562
|
// when it runs fewer than 12 (whole-read form doubles as teaching). Prior prompts
|
|
546
563
|
// stay listed by path in the system packet's User Prompts section — reachable,
|
|
547
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");
|
|
548
570
|
const promptLineCount = promptRow.prompt.split("\n").length;
|
|
549
571
|
const promptRead = {
|
|
550
572
|
op: "READ", suffix: "", signal: null, target: promptPath,
|
|
551
|
-
lineMarker: { marks: promptLineCount >=
|
|
573
|
+
lineMarker: { marks: promptLineCount >= previewLines ? [1, previewLines] : [1, -1] },
|
|
552
574
|
body: null, position: { line: 1, column: 1 },
|
|
553
575
|
};
|
|
554
576
|
await this.dispatch({
|
|
555
|
-
statement: promptRead,
|
|
577
|
+
statement: promptRead, workspaceId, workerId, loopId, turnId,
|
|
556
578
|
sequence: nextActionIndex, origin: "plurnk", onDispatch,
|
|
557
579
|
});
|
|
558
580
|
nextActionIndex++;
|
|
@@ -564,18 +586,18 @@ export default class Engine {
|
|
|
564
586
|
if (seq > 1) {
|
|
565
587
|
const loopSeqRow = await this.#db.engine_loop_sequence.get({ loop_id: loopId });
|
|
566
588
|
const loopSeq = loopSeqRow?.sequence ?? loopId;
|
|
567
|
-
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) });
|
|
568
590
|
const injectedRow = injected.find((r) => typeof r.content === "string" && r.content.length > 0);
|
|
569
591
|
if (injectedRow !== undefined) {
|
|
570
592
|
const lineCount = injectedRow.content.split("\n").length;
|
|
571
|
-
const injTarget = promptTarget(
|
|
593
|
+
const injTarget = promptTarget(workerId, loopSeq, seq);
|
|
572
594
|
const injRead = {
|
|
573
595
|
op: "READ", suffix: "", signal: null, target: injTarget,
|
|
574
596
|
lineMarker: { marks: lineCount >= 12 ? [1, 12] : [1, -1] },
|
|
575
597
|
body: null, position: { line: 1, column: 1 },
|
|
576
598
|
};
|
|
577
599
|
await this.dispatch({
|
|
578
|
-
statement: injRead,
|
|
600
|
+
statement: injRead, workspaceId, workerId, loopId, turnId,
|
|
579
601
|
sequence: nextActionIndex, origin: "plurnk", onDispatch,
|
|
580
602
|
});
|
|
581
603
|
nextActionIndex++;
|
|
@@ -585,46 +607,46 @@ export default class Engine {
|
|
|
585
607
|
// every entry's deep channels (symbols/refs/embeddings/FTS, deep_hash-gated) so the
|
|
586
608
|
// catalog and FIND read current data. NOT an action: no log entry, no sequence slot,
|
|
587
609
|
// not dispatched. There is no plurnk:///manifest.json entry — the catalog is served
|
|
588
|
-
// on demand by FIND(scheme:///**), foisted into the
|
|
610
|
+
// on demand by FIND(scheme:///**), foisted into the worker's first turn below.
|
|
589
611
|
// #312 — the turn's token gauge: the ACTIVE provider's tokenizer identity + exact counter
|
|
590
612
|
// (mimetypes seam; provider upper bound surfaced as tokenizer_unavailable when inexact).
|
|
591
613
|
// Threaded per turn — never engine state — so concurrent loops on different providers
|
|
592
614
|
// each read their own honest numbers.
|
|
593
615
|
const systemCtx = {
|
|
594
|
-
db: this.#db,
|
|
616
|
+
db: this.#db, workspaceId, workerId, loopId, turnId,
|
|
595
617
|
writer: "plurnk",
|
|
596
618
|
signal: this.#loopAborts.get(loopId)?.signal,
|
|
597
619
|
streamEventNotify: this.#streamEventNotify,
|
|
598
|
-
|
|
620
|
+
wakeWorkerNotify: this.#wakeWorkerNotify,
|
|
599
621
|
tokenize: this.#tokenize,
|
|
600
622
|
mimetypes: this.#mimetypes,
|
|
601
623
|
defaultChannelFor: (s) => this.#schemes.defaultChannelFor(s),
|
|
602
|
-
pushTelemetry: (event) => this.#telemetry.push(
|
|
624
|
+
pushTelemetry: (event) => this.#telemetry.push(workspaceId, loopId, event),
|
|
603
625
|
};
|
|
604
626
|
// SPEC §membership D4/D5 — git-ls-files workspace membership, resolved at
|
|
605
627
|
// prompt-composition (EMI is eager + exhaustive — git is the only bound). When the
|
|
606
|
-
//
|
|
628
|
+
// workspace's project_root is a git working tree, tracked files are
|
|
607
629
|
// members without a client `add`; active members are materialized
|
|
608
630
|
// (disk → body channel) so they appear in the catalog. No-ops
|
|
609
|
-
// on headless / non-git
|
|
631
|
+
// on headless / non-git workspaces. Runs BEFORE the derivation pump so
|
|
610
632
|
// this turn's packet reflects them.
|
|
611
633
|
const fsDivergences = await GitMembership.indexGitMembership(systemCtx);
|
|
612
|
-
await this.#logFsFictions(
|
|
634
|
+
await this.#logFsFictions(workspaceId, fsDivergences);
|
|
613
635
|
this.#queueDerivation(() => EntryManifest.maintainDerivations(systemCtx)); // §derivation-off-hot-path — the turn proceeds; ~queries warm their own slice
|
|
614
636
|
// Turn-0 catalog preview (PLURNK_SERVICE_FILES_ITEMS, §actor-boundary-catalog-preview):
|
|
615
|
-
// 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
|
|
616
638
|
// model turn so it opens with its catalog (the per-scheme arrays that replaced the
|
|
617
639
|
// single manifest.json). -1 → each scheme's whole catalog; N → its first N rows
|
|
618
640
|
// (clamped to the scheme's count so FIND's strict <L> never 416s); off by default.
|
|
619
641
|
if (seq === 1) {
|
|
620
|
-
// #231 — a
|
|
621
|
-
const { filesItems:
|
|
622
|
-
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();
|
|
623
645
|
if (filesItems !== null && runFirstLoop) { // #269 — catalog preview is run-once
|
|
624
|
-
// engine_scheme_catalog_summary is the scheme source:
|
|
646
|
+
// engine_scheme_catalog_summary is the scheme source: workspace-scoped, ordered,
|
|
625
647
|
// one row per scheme that has entries (scheme=null → file). log:// is absent —
|
|
626
648
|
// it lives in log_entries, not the catalog (present-mode, the # Log section).
|
|
627
|
-
const catalogSchemes = await this.#db.engine_scheme_catalog_summary.all({
|
|
649
|
+
const catalogSchemes = await this.#db.engine_scheme_catalog_summary.all({ workspace_id: workspaceId });
|
|
628
650
|
// known:/// + unknown:/// + file ALWAYS foist, even at zero entries — else the
|
|
629
651
|
// model burns a turn running the FIND itself, assuming the catalog is merely
|
|
630
652
|
// being withheld. An empty FIND(**) is orienting, not noise (owner): it tells
|
|
@@ -666,7 +688,7 @@ export default class Engine {
|
|
|
666
688
|
position: { line: 1, column: 1 },
|
|
667
689
|
};
|
|
668
690
|
await this.dispatch({
|
|
669
|
-
statement: catalogFind,
|
|
691
|
+
statement: catalogFind, workspaceId, workerId, loopId, turnId,
|
|
670
692
|
sequence: nextActionIndex, origin: "plurnk", onDispatch,
|
|
671
693
|
});
|
|
672
694
|
nextActionIndex++;
|
|
@@ -674,21 +696,21 @@ export default class Engine {
|
|
|
674
696
|
// own survey, mirrored OPEN). The <L> cap rides as `<1,N>`, exactly as the model would type it.
|
|
675
697
|
turnZeroMoves.push(`<<FIND(${isPlurnk ? "plurnk://docs/**" : isFile ? "**" : `${schemeName}:///**`})${cap === null ? "" : `<1,${cap}>`}::FIND`);
|
|
676
698
|
}
|
|
677
|
-
// §
|
|
678
|
-
// 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
|
|
679
701
|
// manage its private workspace) so it's catalogued in ITS perspective alone; other
|
|
680
|
-
// runs reach it only via explicit FIND(
|
|
681
|
-
const
|
|
682
|
-
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;
|
|
683
705
|
if (scratch > 0) {
|
|
684
706
|
const runFind = {
|
|
685
707
|
op: "FIND", suffix: "", signal: null,
|
|
686
|
-
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 },
|
|
687
709
|
body: null, lineMarker: null, position: { line: 1, column: 1 },
|
|
688
710
|
};
|
|
689
|
-
await this.dispatch({ statement: runFind,
|
|
711
|
+
await this.dispatch({ statement: runFind, workspaceId, workerId, loopId, turnId, sequence: nextActionIndex, origin: "plurnk", onDispatch });
|
|
690
712
|
nextActionIndex++;
|
|
691
|
-
turnZeroMoves.push("<<FIND(
|
|
713
|
+
turnZeroMoves.push("<<FIND(worker://self/**)::FIND"); // §model-entry — the worker-scope survey, into the turn-0 echo
|
|
692
714
|
}
|
|
693
715
|
}
|
|
694
716
|
// #260 — foist a turn-0 READ of each client-passed @file path so its content sits in front
|
|
@@ -707,7 +729,7 @@ export default class Engine {
|
|
|
707
729
|
body: null, position: { line: 1, column: 1 },
|
|
708
730
|
};
|
|
709
731
|
await this.dispatch({
|
|
710
|
-
statement: fileRead,
|
|
732
|
+
statement: fileRead, workspaceId, workerId, loopId, turnId,
|
|
711
733
|
sequence: nextActionIndex, origin: "plurnk", onDispatch,
|
|
712
734
|
});
|
|
713
735
|
nextActionIndex++;
|
|
@@ -718,34 +740,34 @@ export default class Engine {
|
|
|
718
740
|
// so the grammar can stay thin. Subsequent turns mirror the model's real output, folded.
|
|
719
741
|
if (runFirstLoop) {
|
|
720
742
|
const emission = ["<<PLAN:Initialize:PLAN", ...turnZeroMoves, "<<SEND[102]:Initialized:SEND"].join("\n");
|
|
721
|
-
await this.#dispatcher.writeModelEntry({ verbatim: emission,
|
|
743
|
+
await this.#dispatcher.writeModelEntry({ verbatim: emission, workerId, loopId, turnId, sequence: 1, folded: false, origin: "plurnk" });
|
|
722
744
|
}
|
|
723
745
|
}
|
|
724
|
-
// §env-delta — pre-seed the
|
|
746
|
+
// §env-delta — pre-seed the worker's ambient observations (what changed since
|
|
725
747
|
// it last looked) as foisted rows before the packet composes; advance the action index
|
|
726
748
|
// past them so model ops continue after. Two instances of one machine: env-delta (sibling
|
|
727
749
|
// edits · timestamp cursor · always folded) and exec streams (channel bytes · byte cursor ·
|
|
728
750
|
// terminal delta opens). §env-delta §exec-stream
|
|
729
|
-
// §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
|
|
730
752
|
// from a prior turn — this runs before the turn's own spawns) so a `<0>` never survives into
|
|
731
753
|
// the subsequent turn. The terminal output then surfaces born-OPEN via the stream-delta path.
|
|
732
|
-
await this.#reapTurnScopedStreams(
|
|
733
|
-
nextActionIndex += await this.#materializeEnvironmentDeltas({
|
|
734
|
-
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 });
|
|
735
757
|
// SPEC §telemetry — git working-tree state for the telemetry section, read once
|
|
736
758
|
// (a service-side `git status` shell-out) and threaded into the budget
|
|
737
759
|
// rebuild too so it isn't re-shelled on overflow.
|
|
738
|
-
const gitStatus = await GitState.status(this.#db,
|
|
760
|
+
const gitStatus = await GitState.status(this.#db, workspaceId, this.#loopAborts.get(loopId)?.signal);
|
|
739
761
|
// Build the spec'd packet (Packet.json) request half. The log build
|
|
740
|
-
// queries log_entries scoped to the
|
|
762
|
+
// queries log_entries scoped to the worker — the prompt entry just
|
|
741
763
|
// written (if turn 1) is part of that query result.
|
|
742
764
|
let requestPacket = await this.#packets.buildRequestPacket({
|
|
743
|
-
initialMessages: messages, requirements,
|
|
765
|
+
initialMessages: messages, requirements, workspaceId, workerId, loopId,
|
|
744
766
|
currentTurnSeq: seq, provider, gitStatus,
|
|
745
767
|
});
|
|
746
768
|
// SPEC §grinder — budget grinder, pre-LLM: reclaim window on actual overflow.
|
|
747
769
|
const enforced = await this.#packets.enforceBudget({
|
|
748
|
-
packet: requestPacket, provider,
|
|
770
|
+
packet: requestPacket, provider, workerId, loopId, turnId,
|
|
749
771
|
// The overflow error row is minted at the turn's running sequence (nextActionIndex), pre-generate;
|
|
750
772
|
// runTurn advances the counter past it below so the post-generate dispatch rows never collide.
|
|
751
773
|
mintSequence: nextActionIndex,
|
|
@@ -753,7 +775,7 @@ export default class Engine {
|
|
|
753
775
|
// overflow row just minted surfaces THIS turn (§grinder-overflow-error-row). Safe: the
|
|
754
776
|
// ephemeral buffer is empty pre-generate (events drain on the next turn's build).
|
|
755
777
|
rebuild: () => this.#packets.buildRequestPacket({
|
|
756
|
-
initialMessages: messages, requirements,
|
|
778
|
+
initialMessages: messages, requirements, workspaceId, workerId, loopId,
|
|
757
779
|
currentTurnSeq: seq, provider, gitStatus,
|
|
758
780
|
}),
|
|
759
781
|
});
|
|
@@ -771,13 +793,13 @@ export default class Engine {
|
|
|
771
793
|
// overflow terminates 413 — death only after the model was told. Physically
|
|
772
794
|
// unsendable (over the provider's real window too) → 413 immediately; physics
|
|
773
795
|
// doesn't negotiate. The pointer stays at 100% of budget — a margin would mask it.
|
|
774
|
-
const physicallySendable = provider.
|
|
796
|
+
const physicallySendable = provider.contextWindow === null
|
|
775
797
|
? true
|
|
776
|
-
: this.#packets.exactPacketTokens(requestPacket, provider) <= provider.
|
|
798
|
+
: this.#packets.exactPacketTokens(requestPacket, provider) <= provider.contextWindow - (this.#packets.maxTokensFor(provider) ?? 0);
|
|
777
799
|
if (physicallySendable && !this.#hardOverflowRecovery.has(loopId)) {
|
|
778
800
|
this.#hardOverflowRecovery.add(loopId);
|
|
779
801
|
await this.#db.engine_insert_log_entry.get({
|
|
780
|
-
|
|
802
|
+
worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence: nextActionIndex++,
|
|
781
803
|
origin: "model", source: "engine", op: "error", suffix: "", signal: null,
|
|
782
804
|
scheme: null, username: null, password: null, hostname: null, port: null,
|
|
783
805
|
pathname: null, params: null, fragment: null, lineMarker: null,
|
|
@@ -790,7 +812,7 @@ export default class Engine {
|
|
|
790
812
|
// errors sections (the same re-derive contract the soft grind uses).
|
|
791
813
|
nextActionIndex += 1;
|
|
792
814
|
requestPacket = await this.#packets.buildRequestPacket({
|
|
793
|
-
initialMessages: messages, requirements,
|
|
815
|
+
initialMessages: messages, requirements, workspaceId, workerId, loopId,
|
|
794
816
|
currentTurnSeq: seq, provider, gitStatus,
|
|
795
817
|
});
|
|
796
818
|
}
|
|
@@ -801,7 +823,7 @@ export default class Engine {
|
|
|
801
823
|
await this.#db.engine_close_turn.run({
|
|
802
824
|
id: turnId, status: 413, packet: JSON.stringify(hardPacket),
|
|
803
825
|
usage_prompt: 0, usage_completion: 0, usage_reasoning: 0, usage_cached: 0, usage_cost_pico: 0,
|
|
804
|
-
|
|
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
|
|
805
827
|
finish_reason: "budget_hard_stop", model: provider.model, meta: "{}",
|
|
806
828
|
});
|
|
807
829
|
return { turnId, status: 413, statuses: [], fingerprint: "", budgetStruck: enforced.struck, budgetHardStop: true, steerStruck: false };
|
|
@@ -817,7 +839,7 @@ export default class Engine {
|
|
|
817
839
|
// the input under the ceiling); the model's decode — reasoning + emission — is
|
|
818
840
|
// out of band, owned by the provider's own context window. Deriving a maxTokens
|
|
819
841
|
// from our budget conflated the two and guillotined a reasoning model's
|
|
820
|
-
// out-of-band
|
|
842
|
+
// out-of-band reasoning as the packet filled (`ceiling - packet` → near-zero
|
|
821
843
|
// decode → finish=length mid-reasoning → no emission → strike spiral). The
|
|
822
844
|
// provider enforces its physical wall on its own.
|
|
823
845
|
let response;
|
|
@@ -827,8 +849,8 @@ export default class Engine {
|
|
|
827
849
|
// #249 — tag the loop (the activity) with its active plugins' attribution tags, write-once.
|
|
828
850
|
if (attributions.length > 0)
|
|
829
851
|
await this.#db.engine_tag_loop_attributions.run({ loop_id: loopId, attributions: JSON.stringify(attributions) });
|
|
830
|
-
// #249 —
|
|
831
|
-
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);
|
|
832
854
|
try {
|
|
833
855
|
// §turn-lifecycle (#301) — the provider call is the long, opaque window (submit → first
|
|
834
856
|
// committed op is provider latency + a full first-turn generation, ~70s local): a static
|
|
@@ -837,13 +859,19 @@ export default class Engine {
|
|
|
837
859
|
// ops are about to land. Base telemetry/event channel (the embed_progress precedent, §tokenomics
|
|
838
860
|
// clients already render it unconditionally); the abort guard keeps a cancelled loop silent.
|
|
839
861
|
if (!signal?.aborted)
|
|
840
|
-
this.#telemetry.push(
|
|
862
|
+
this.#telemetry.push(workspaceId, loopId, { source: "engine:turn", kind: "turn_awaiting_model", level: "info", message: "awaiting model response" });
|
|
841
863
|
// generate rides the LOOP signal (already chained from the caller's), so a loop-level
|
|
842
864
|
// abort — the §operator-config-loop-timeout wall — cancels a stuck provider call, not
|
|
843
865
|
// just the schemes. Bare runTurn (no runLoop) has no loop entry → the caller's signal.
|
|
844
|
-
|
|
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) ?? undefined, 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
|
|
845
873
|
if (!signal?.aborted)
|
|
846
|
-
this.#telemetry.push(
|
|
874
|
+
this.#telemetry.push(workspaceId, loopId, { source: "engine:turn", kind: "turn_generated", level: "info", message: "parsing model response" });
|
|
847
875
|
}
|
|
848
876
|
catch (err) {
|
|
849
877
|
// §turn-never-blank — a ProviderError is an INFRASTRUCTURE failure (auth, network
|
|
@@ -857,7 +885,7 @@ export default class Engine {
|
|
|
857
885
|
// adjudication into a model-behavior 422 — a state the system otherwise forbids,
|
|
858
886
|
// a record that lied, and days of forensics pointed at the wrong suspect.
|
|
859
887
|
if (err instanceof ProviderError) {
|
|
860
|
-
this.#telemetry.push(
|
|
888
|
+
this.#telemetry.push(workspaceId, loopId, { source: "provider", kind: err.kind, message: err.message, level: "error" });
|
|
861
889
|
}
|
|
862
890
|
throw err;
|
|
863
891
|
}
|
|
@@ -893,7 +921,7 @@ export default class Engine {
|
|
|
893
921
|
const located = typeof event.position === "number"
|
|
894
922
|
? this.#offsetToLineColumn(packetAssistant.content, event.position)
|
|
895
923
|
: null;
|
|
896
|
-
this.#telemetry.push(
|
|
924
|
+
this.#telemetry.push(workspaceId, loopId, {
|
|
897
925
|
source: event.source,
|
|
898
926
|
kind: event.kind,
|
|
899
927
|
message: event.message ?? "",
|
|
@@ -931,8 +959,14 @@ export default class Engine {
|
|
|
931
959
|
: realOpsCount === 0 ? TURN_STATUS_NO_OPS : TURN_STATUS_IMPLICIT_CONTINUE;
|
|
932
960
|
// Idle turn: an implicit-continue (102) that did no WORK — its ops are only PLAN/SEND, no mid op.
|
|
933
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.
|
|
934
968
|
const midOpsCount = packetAssistant.ops.filter((op) => op.op !== "PLAN" && op.op !== "SEND").length;
|
|
935
|
-
if (!steerStruck && turnStatus === TURN_STATUS_IMPLICIT_CONTINUE && midOpsCount === 0) {
|
|
969
|
+
if (!steerStruck && turnStatus === TURN_STATUS_IMPLICIT_CONTINUE && midOpsCount === 0 && parseErrors.length === 0) {
|
|
936
970
|
// One grace turn after a retrieval-only 409 (admins specimen): the refusal steer says
|
|
937
971
|
// "continuing in order to receive results" — a model that obediently waits one bare
|
|
938
972
|
// [102] turn is following OUR advice, and the idle rail was executing it for that.
|
|
@@ -960,7 +994,7 @@ export default class Engine {
|
|
|
960
994
|
usage_reasoning: usage.reasoning,
|
|
961
995
|
usage_cached: usage.cached,
|
|
962
996
|
usage_cost_pico: provider.costFor(usage), // §provider-surface-costfor
|
|
963
|
-
|
|
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
|
|
964
998
|
finish_reason: finishReason,
|
|
965
999
|
model,
|
|
966
1000
|
// #252 — opaque provider→client metadata passthrough (e.g. balancePico the
|
|
@@ -970,15 +1004,13 @@ export default class Engine {
|
|
|
970
1004
|
// Dispatch model ops starting at nextActionIndex (continues the
|
|
971
1005
|
// turn's running counter after any pre-model writes).
|
|
972
1006
|
//
|
|
973
|
-
// Max-commands
|
|
974
|
-
//
|
|
975
|
-
//
|
|
976
|
-
//
|
|
977
|
-
//
|
|
978
|
-
//
|
|
979
|
-
|
|
980
|
-
// #232 — a session's maxCommands is a tighten-only ceiling: min() the env ceiling.
|
|
981
|
-
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);
|
|
982
1014
|
// PLAN (reasoning) and a terminal SEND (signal ≥ 200, the conclusion) are not
|
|
983
1015
|
// actions — they always dispatch and never count against the cap. maxCommands
|
|
984
1016
|
// bounds real actions only; maxCommands:0 still admits a plan and a conclusion
|
|
@@ -995,7 +1027,7 @@ export default class Engine {
|
|
|
995
1027
|
let rowSeq = nextActionIndex;
|
|
996
1028
|
for (const statement of opsToDispatch) {
|
|
997
1029
|
const result = await this.dispatch({
|
|
998
|
-
statement,
|
|
1030
|
+
statement, workspaceId, workerId, loopId, turnId,
|
|
999
1031
|
sequence: rowSeq,
|
|
1000
1032
|
origin, onDispatch,
|
|
1001
1033
|
// §send-200-failed-ops — parse errors mint as rows AFTER this loop; the terminal
|
|
@@ -1036,25 +1068,33 @@ export default class Engine {
|
|
|
1036
1068
|
if (droppedCount > 0)
|
|
1037
1069
|
pendingEngineErrors.push("max_commands_exceeded");
|
|
1038
1070
|
for (const kind of pendingEngineErrors)
|
|
1039
|
-
await this.#telemetry.mintEngineError(kind, {
|
|
1071
|
+
await this.#telemetry.mintEngineError(kind, { workerId, loopId, turnId, sequence: errSeq++ });
|
|
1040
1072
|
// §log-row-self-explains (Q2, owner-clarified) — a model-op failure is the MODEL'S OWN op
|
|
1041
1073
|
// result: the op row carries its failure message on its meta line (packet-wire), and the
|
|
1042
1074
|
// errors section points at the row. No separate minted item (the retired action_failure
|
|
1043
1075
|
// mint dressed op results as source:"engine" faults — the jumbo model chased a phantom
|
|
1044
1076
|
// "engine run 400 error" off a message-less item). Genuine engine-internal faults CRASH
|
|
1045
1077
|
// (fail-hard, §turn-never-blank) and never mint model-facing rows.
|
|
1046
|
-
// §tokenomics-output-truncated
|
|
1047
|
-
//
|
|
1048
|
-
//
|
|
1049
|
-
//
|
|
1050
|
-
|
|
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`;
|
|
1051
1091
|
await this.#db.engine_insert_log_entry.get({
|
|
1052
|
-
|
|
1092
|
+
worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence: errSeq++,
|
|
1053
1093
|
origin: "model", source: "engine", op: "error", suffix: "", signal: null,
|
|
1054
1094
|
scheme: null, username: null, password: null, hostname: null, port: null,
|
|
1055
1095
|
pathname: null, params: null, fragment: null, lineMarker: null,
|
|
1056
1096
|
tx: "", mimetype_tx: "text/plain",
|
|
1057
|
-
rx: JSON.stringify({ status: 413, kind: "output_truncated", message
|
|
1097
|
+
rx: JSON.stringify({ status: 413, kind: "output_truncated", message }),
|
|
1058
1098
|
mimetype_rx: "application/json", status_rx: 413, tokens: 0, state: "failed", outcome: "output_truncated",
|
|
1059
1099
|
attrs: "{}",
|
|
1060
1100
|
});
|
|
@@ -1063,7 +1103,7 @@ export default class Engine {
|
|
|
1063
1103
|
// resolved against the model's folded mirror row (§model-entry) — origin 'model', not engine.
|
|
1064
1104
|
for (const { message, line, column, source } of parseErrors ?? []) {
|
|
1065
1105
|
await this.#db.engine_insert_log_entry.get({
|
|
1066
|
-
|
|
1106
|
+
worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence: errSeq++,
|
|
1067
1107
|
origin: "model", source: "grammar", op: "error", suffix: "", signal: null,
|
|
1068
1108
|
scheme: null, username: null, password: null, hostname: null, port: null,
|
|
1069
1109
|
pathname: null, params: null, fragment: null, lineMarker: null,
|
|
@@ -1084,8 +1124,9 @@ export default class Engine {
|
|
|
1084
1124
|
// else does: the model that cares READs the folded row at the lines it wants — and can
|
|
1085
1125
|
// introspect any prior emission of its own the same way. Empty emissions (a struck/
|
|
1086
1126
|
// silent turn) write nothing — no prior output to mirror.
|
|
1087
|
-
|
|
1088
|
-
|
|
1127
|
+
const sealed = response.assistant.reasoningEncrypted;
|
|
1128
|
+
if (packetAssistant.content.trim().length > 0 || (sealed !== undefined && sealed.length > 0)) {
|
|
1129
|
+
await this.#dispatcher.writeModelEntry({ verbatim: packetAssistant.content, workerId, loopId, turnId, sequence: errSeq++, folded: true, ...(sealed !== undefined && sealed.length > 0 ? { reasoningEncrypted: sealed } : {}) });
|
|
1089
1130
|
}
|
|
1090
1131
|
// Zero ops is NOT an error to report — the model knows it emitted
|
|
1091
1132
|
// nothing. Strike accounting (engine-internal) treats it as a
|
|
@@ -1159,40 +1200,40 @@ export default class Engine {
|
|
|
1159
1200
|
}
|
|
1160
1201
|
// #note12 — the daughter-provided reference docs (schemes' + execs' `documentation`),
|
|
1161
1202
|
// materialized at plurnk:///docs/<name>.md by loop_run (like operator docs).
|
|
1162
|
-
docEntries(
|
|
1163
|
-
return this.#packets.docEntries(
|
|
1203
|
+
docEntries(workspaceId) {
|
|
1204
|
+
return this.#packets.docEntries(workspaceId);
|
|
1164
1205
|
}
|
|
1165
1206
|
// §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
|
|
1166
|
-
// run last looked. No per-
|
|
1207
|
+
// run last looked. No per-worker snapshot (§machine-processes "a worker is its log"): every
|
|
1167
1208
|
// edit is already a span-carrying log row, so PULL other actors' EDITs on shared
|
|
1168
|
-
// entries since this
|
|
1209
|
+
// entries since this worker's prior turn — real cross-worker edits and the plurnk worker's
|
|
1169
1210
|
// fs-sync fictions — and materialize each as a FOLDED delta reusing the row's span +
|
|
1170
1211
|
// cause. Returns the count so the caller advances nextActionIndex past the deltas.
|
|
1171
1212
|
async #materializeEnvironmentDeltas(args) {
|
|
1172
|
-
const {
|
|
1173
|
-
const boundary = await this.#db.
|
|
1213
|
+
const { workspaceId, workerId, loopId, turnId, fromSequence } = args;
|
|
1214
|
+
const boundary = await this.#db.engine_worker_prior_turn_time.get({ worker_id: workerId, turn_id: turnId });
|
|
1174
1215
|
const since = boundary?.since ?? null;
|
|
1175
1216
|
if (since === null)
|
|
1176
1217
|
return 0; // first turn — nothing prior; the model reads current state fresh
|
|
1177
|
-
const rows = await this.#db.engine_pull_env_deltas.all({
|
|
1218
|
+
const rows = await this.#db.engine_pull_env_deltas.all({ workspace_id: workspaceId, worker_id: workerId, since });
|
|
1178
1219
|
let written = 0;
|
|
1179
1220
|
for (const r of rows) {
|
|
1180
|
-
// source: the originating run (a real cross-
|
|
1221
|
+
// source: the originating run (a real cross-worker edit) or 'file' (an fs fiction);
|
|
1181
1222
|
// rx reuses the originating row's result span — the edit as it looked then.
|
|
1182
1223
|
await this.#db.engine_insert_env_delta.run({
|
|
1183
|
-
|
|
1184
|
-
source: r.source ?? String(r.
|
|
1224
|
+
worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence: fromSequence + written,
|
|
1225
|
+
source: r.source ?? String(r.worker_id), scheme: r.scheme, pathname: r.pathname, rx: r.rx,
|
|
1185
1226
|
});
|
|
1186
1227
|
written++;
|
|
1187
1228
|
}
|
|
1188
|
-
// §
|
|
1229
|
+
// §worker-scheme — loop-terminations: a sibling's loop reaching terminal surfaces the
|
|
1189
1230
|
// same way an entry-change does, carrying its deliverable (the SEND body) or the
|
|
1190
1231
|
// abandonment reason. Folded, attributed to the terminated run.
|
|
1191
|
-
const terms = await this.#db.engine_pull_loop_terminations.all({
|
|
1232
|
+
const terms = await this.#db.engine_pull_loop_terminations.all({ workspace_id: workspaceId, worker_id: workerId, since });
|
|
1192
1233
|
for (const t of terms) {
|
|
1193
1234
|
await this.#db.engine_insert_loop_termination_delta.run({
|
|
1194
|
-
|
|
1195
|
-
source: String(t.
|
|
1235
|
+
worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence: fromSequence + written,
|
|
1236
|
+
source: String(t.worker_id), pathname: `/${t.worker_name}`,
|
|
1196
1237
|
rx: markTerminal(t.terminated_by, t.terminal_message) ?? `loop "${t.prompt}" ended (${t.status})`,
|
|
1197
1238
|
status: t.status,
|
|
1198
1239
|
});
|
|
@@ -1200,13 +1241,13 @@ export default class Engine {
|
|
|
1200
1241
|
}
|
|
1201
1242
|
return written;
|
|
1202
1243
|
}
|
|
1203
|
-
// §exec-poll — EXEC `<0>` is turn-scoped: abort the
|
|
1244
|
+
// §exec-poll — EXEC `<0>` is turn-scoped: abort the worker's open turn-scoped streams via their
|
|
1204
1245
|
// owning scheme (the same registry-routed abort the total reap uses). Called at each pre-turn
|
|
1205
1246
|
// before the turn's own spawns, so every open turn-scoped sub here is from a prior turn — it
|
|
1206
1247
|
// never survives into the subsequent turn. Fire-and-forget: the spawn finalizes async and its
|
|
1207
1248
|
// terminal output surfaces born-OPEN through the stream-delta path (§exec-stream).
|
|
1208
|
-
async #reapTurnScopedStreams(
|
|
1209
|
-
const open = await this.#db.
|
|
1249
|
+
async #reapTurnScopedStreams(workerId) {
|
|
1250
|
+
const open = await this.#db.find_open_turn_scoped_subscriptions_for_worker.all({ worker_id: workerId });
|
|
1210
1251
|
for (const { id, scheme } of open) {
|
|
1211
1252
|
const handler = this.#schemes.get(scheme);
|
|
1212
1253
|
handler?.abortSubscription?.(id);
|
|
@@ -1218,12 +1259,12 @@ export default class Engine {
|
|
|
1218
1259
|
// streamEnd recorded on the channel's prior delta — no exec-specific surfacing, just the
|
|
1219
1260
|
// env-observe loop with a byte cursor where env-delta uses a timestamp. §exec-stream
|
|
1220
1261
|
async #materializeStreamDeltas(args) {
|
|
1221
|
-
const {
|
|
1222
|
-
const channels = await this.#db.
|
|
1262
|
+
const { workerId, loopId, turnId, fromSequence } = args;
|
|
1263
|
+
const channels = await this.#db.engine_worker_stream_channels.all({ worker_id: workerId });
|
|
1223
1264
|
let written = 0;
|
|
1224
1265
|
for (const ch of channels) {
|
|
1225
1266
|
const prior = await this.#db.engine_stream_cursor.get({
|
|
1226
|
-
|
|
1267
|
+
worker_id: workerId, scheme: ch.runtime, pathname: ch.coord, fragment: ch.channel,
|
|
1227
1268
|
});
|
|
1228
1269
|
const priorAttrs = prior !== undefined ? JSON.parse(prior.attrs) : {};
|
|
1229
1270
|
const cursor = priorAttrs.streamEnd ?? 0;
|
|
@@ -1237,7 +1278,7 @@ export default class Engine {
|
|
|
1237
1278
|
// pointer to the already-delivered bytes, never a re-send (§tokenomics-fetch-fits-free).
|
|
1238
1279
|
if (closed && priorAttrs.terminal !== true && cursor > 0) {
|
|
1239
1280
|
await this.#db.engine_insert_stream_delta.run({
|
|
1240
|
-
|
|
1281
|
+
worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence: fromSequence + written,
|
|
1241
1282
|
scheme: ch.runtime, pathname: ch.coord, fragment: ch.channel,
|
|
1242
1283
|
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" }),
|
|
1243
1284
|
attrs: JSON.stringify({ streamEnd: ch.content.length, terminal: true }),
|
|
@@ -1251,7 +1292,7 @@ export default class Engine {
|
|
|
1251
1292
|
// into one sequence (lines N..M, then M+1..), not N independent "1:" restarts. §exec-stream
|
|
1252
1293
|
const startLine = (ch.content.slice(0, cursor).match(/\n/g)?.length ?? 0) + 1;
|
|
1253
1294
|
await this.#db.engine_insert_stream_delta.run({
|
|
1254
|
-
|
|
1295
|
+
worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence: fromSequence + written,
|
|
1255
1296
|
scheme: ch.runtime, pathname: ch.coord, fragment: ch.channel,
|
|
1256
1297
|
rx: JSON.stringify({ status: 200, content: ch.content.slice(cursor), mimetype: "text/stream", startLine }),
|
|
1257
1298
|
attrs: JSON.stringify({ streamEnd: ch.content.length, terminal: closed }),
|
|
@@ -1262,20 +1303,20 @@ export default class Engine {
|
|
|
1262
1303
|
return written;
|
|
1263
1304
|
}
|
|
1264
1305
|
// §env-delta — the filesystem as an actor. Ambient disk divergences detected at
|
|
1265
|
-
// pre-turn (git membership re-read) are logged as the plurnk
|
|
1306
|
+
// pre-turn (git membership re-read) are logged as the plurnk worker's source=file EDIT
|
|
1266
1307
|
// "fictions": no op happened, but EDIT is the only grammar the model has for "your
|
|
1267
1308
|
// world changed," so the fiction keeps its perspective aligned with what its tooling
|
|
1268
|
-
// would show. The fiction lives in the plurnk
|
|
1309
|
+
// would show. The fiction lives in the plurnk worker's log; every other run pulls it
|
|
1269
1310
|
// through the one delta path, exactly like a sibling's real edit.
|
|
1270
|
-
// §membership-emi-divergence-signal — disk divergences logged as the plurnk
|
|
1271
|
-
async #logFsFictions(
|
|
1311
|
+
// §membership-emi-divergence-signal — disk divergences logged as the plurnk worker's source=file EDIT fictions
|
|
1312
|
+
async #logFsFictions(workspaceId, divergences) {
|
|
1272
1313
|
if (divergences.length === 0)
|
|
1273
1314
|
return;
|
|
1274
|
-
const run = await this.#db.
|
|
1275
|
-
?? await this.#db.
|
|
1315
|
+
const run = await this.#db.envelope_get_worker_by_name.get({ workspace_id: workspaceId, name: "plurnk" })
|
|
1316
|
+
?? await this.#db.envelope_insert_worker.get({ workspace_id: workspaceId, name: "plurnk", origin: "plurnk" });
|
|
1276
1317
|
if (run === undefined)
|
|
1277
|
-
throw new Error("logFsFictions: plurnk
|
|
1278
|
-
const loop = await this.#db.envelope_insert_client_loop.get({
|
|
1318
|
+
throw new Error("logFsFictions: plurnk worker resolution returned no row");
|
|
1319
|
+
const loop = await this.#db.envelope_insert_client_loop.get({ worker_id: run.id });
|
|
1279
1320
|
if (loop === undefined)
|
|
1280
1321
|
throw new Error("logFsFictions: loop insert returned no row");
|
|
1281
1322
|
const seq = await this.#db.client_turn_next_sequence.get({ loop_id: loop.id });
|
|
@@ -1286,7 +1327,7 @@ export default class Engine {
|
|
|
1286
1327
|
for (const d of divergences) {
|
|
1287
1328
|
const span = editedSpan(d.before, d.after);
|
|
1288
1329
|
await this.#db.engine_insert_log_entry.get({
|
|
1289
|
-
|
|
1330
|
+
worker_id: run.id, loop_id: loop.id, turn_id: turn.id, sequence: sequence++,
|
|
1290
1331
|
origin: "plurnk", source: "file", op: "EDIT", suffix: "", signal: null,
|
|
1291
1332
|
scheme: d.scheme, username: null, password: null, hostname: null, port: null,
|
|
1292
1333
|
pathname: d.pathname, params: null, fragment: null, lineMarker: null,
|
|
@@ -1328,79 +1369,63 @@ export default class Engine {
|
|
|
1328
1369
|
// run still accepting turns?" If yes, skip the wake — the active loop
|
|
1329
1370
|
// will pick up the channel transition at its next turn boundary. If no,
|
|
1330
1371
|
// the daemon opens a fresh loop with the wake prompt.
|
|
1331
|
-
async
|
|
1332
|
-
const row = await this.#db.
|
|
1372
|
+
async hasActiveLoopForWorker(workerId) {
|
|
1373
|
+
const row = await this.#db.engine_count_active_loops_for_worker.get({ worker_id: workerId });
|
|
1333
1374
|
return (row?.n ?? 0) > 0;
|
|
1334
1375
|
}
|
|
1335
1376
|
// #290 — run the derivation pump (deep channels: symbols/refs/FTS +
|
|
1336
|
-
// embeddings, deep_hash-gated) at
|
|
1337
|
-
//
|
|
1338
|
-
// loop.run.
|
|
1377
|
+
// embeddings, deep_hash-gated) at WORKSPACE-SCOPE, off the per-turn path, so a freshly-created
|
|
1378
|
+
// workspace's corpus warms DURING the client's startup window instead of freezing the first
|
|
1379
|
+
// loop.run. workspace.create fires this and returns immediately; embed_progress live-fans-out as it
|
|
1339
1380
|
// runs. Idempotent + deep_hash-gated, so turn 1's pump finds the work done (or harmlessly re-runs);
|
|
1340
1381
|
// a no-embedder build derives the cheap symbols/refs/FTS channels and skips the embed pass. Has no
|
|
1341
1382
|
// loop yet — telemetry fans out live only (loopId 0), never buffered to a loop that never drains.
|
|
1342
|
-
async
|
|
1383
|
+
async warmWorkspaceDerivations(workspaceId) {
|
|
1343
1384
|
const ctx = {
|
|
1344
|
-
db: this.#db,
|
|
1385
|
+
db: this.#db, workspaceId, workerId: 0, loopId: 0, turnId: 0,
|
|
1345
1386
|
writer: "plurnk",
|
|
1346
1387
|
signal: undefined,
|
|
1347
1388
|
streamEventNotify: this.#streamEventNotify,
|
|
1348
|
-
|
|
1389
|
+
wakeWorkerNotify: this.#wakeWorkerNotify,
|
|
1349
1390
|
tokenize: this.#tokenize,
|
|
1350
1391
|
mimetypes: this.#mimetypes,
|
|
1351
1392
|
defaultChannelFor: (s) => this.#schemes.defaultChannelFor(s),
|
|
1352
|
-
pushTelemetry: (event) => this.#telemetry.notify(
|
|
1393
|
+
pushTelemetry: (event) => this.#telemetry.notify(workspaceId, 0, event),
|
|
1353
1394
|
};
|
|
1354
1395
|
await this.#queueDerivation(() => EntryManifest.maintainDerivations(ctx)); // §derivation-off-hot-path
|
|
1355
1396
|
}
|
|
1356
|
-
// Inject a prompt into the
|
|
1397
|
+
// Inject a prompt into the worker's currently-executing loop. Writes a
|
|
1357
1398
|
// plurnk://prompt/<run>/<loop>/<next-turn> entry whose body becomes the
|
|
1358
1399
|
// prompt section at the next turn boundary. Last-wins: if two
|
|
1359
1400
|
// injects target the same next-turn slot, the second overwrites the
|
|
1360
1401
|
// first.
|
|
1361
1402
|
//
|
|
1362
|
-
// Returns null when no loop in the
|
|
1403
|
+
// Returns null when no loop in the worker is currently active (status=102).
|
|
1363
1404
|
// The daemon-side inject path then enqueues a fresh loop with this
|
|
1364
1405
|
// prompt; engine doesn't open loops itself.
|
|
1365
1406
|
//
|
|
1366
1407
|
// Rummy parallel: AgentLoop.inject(). The "active drain → write
|
|
1367
1408
|
// prompt entry, return immediately" branch.
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
async prefetchCtx(sessionId, runId) {
|
|
1371
|
-
const loop = await this.#db.drain_current_loop_for_run.get({ run_id: runId });
|
|
1372
|
-
const loopId = loop?.id ?? 0;
|
|
1373
|
-
return {
|
|
1374
|
-
db: this.#db, sessionId, runId, loopId, turnId: 0,
|
|
1375
|
-
writer: "plurnk",
|
|
1376
|
-
signal: this.#loopAborts.get(loopId)?.signal,
|
|
1377
|
-
streamEventNotify: this.#streamEventNotify,
|
|
1378
|
-
wakeRunNotify: this.#wakeRunNotify,
|
|
1379
|
-
tokenize: this.#tokenize,
|
|
1380
|
-
mimetypes: this.#mimetypes,
|
|
1381
|
-
pushTelemetry: (event) => this.#telemetry.push(sessionId, loopId, event),
|
|
1382
|
-
};
|
|
1383
|
-
}
|
|
1384
|
-
async inject(runId, prompt) {
|
|
1385
|
-
const loopRow = await this.#db.drain_current_loop_for_run.get({ run_id: runId });
|
|
1409
|
+
async inject(workerId, prompt) {
|
|
1410
|
+
const loopRow = await this.#db.drain_current_loop_for_worker.get({ worker_id: workerId });
|
|
1386
1411
|
if (loopRow === undefined)
|
|
1387
1412
|
return null;
|
|
1388
1413
|
const loopId = loopRow.id;
|
|
1389
1414
|
const turnRow = await this.#db.drain_next_turn_seq_for_loop.get({ loop_id: loopId });
|
|
1390
1415
|
const turnSeq = turnRow?.next ?? 1;
|
|
1391
|
-
const
|
|
1392
|
-
if (
|
|
1393
|
-
throw new Error(`Engine.inject: run ${
|
|
1394
|
-
const pathname = promptPathname(
|
|
1416
|
+
const workspaceRow = await this.#db.drain_get_worker_workspace.get({ worker_id: workerId });
|
|
1417
|
+
if (workspaceRow === undefined)
|
|
1418
|
+
throw new Error(`Engine.inject: run ${workerId} not found`);
|
|
1419
|
+
const pathname = promptPathname(workerId, loopRow.sequence, turnSeq); // canonical storage form, worker-qualified loop-SEQ coordinates matching the turn-1 foist
|
|
1395
1420
|
const ctx = {
|
|
1396
|
-
db: this.#db,
|
|
1421
|
+
db: this.#db, workspaceId: workspaceRow.workspace_id, workerId, loopId,
|
|
1397
1422
|
turnId: 0, // no turn open at inject time; entries don't pin turnId
|
|
1398
1423
|
writer: "plurnk",
|
|
1399
1424
|
signal: this.#loopAborts.get(loopId)?.signal,
|
|
1400
1425
|
streamEventNotify: this.#streamEventNotify,
|
|
1401
|
-
|
|
1426
|
+
wakeWorkerNotify: this.#wakeWorkerNotify,
|
|
1402
1427
|
tokenize: this.#tokenize,
|
|
1403
|
-
pushTelemetry: (event) => this.#telemetry.push(
|
|
1428
|
+
pushTelemetry: (event) => this.#telemetry.push(workspaceRow.workspace_id, loopId, event),
|
|
1404
1429
|
};
|
|
1405
1430
|
const entry = {
|
|
1406
1431
|
channels: { body: { content: prompt, mimetype: "text/markdown" } },
|