@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/Dispatcher.js
CHANGED
|
@@ -2,13 +2,13 @@ var _a;
|
|
|
2
2
|
import { parsePath } from "@plurnk/plurnk-grammar";
|
|
3
3
|
import { foldAuthorityIntoPath, schemeNameOf } from "./plurnk-uri.js";
|
|
4
4
|
import Fork from "./fork.js";
|
|
5
|
-
import
|
|
5
|
+
import WorkerCap from "./worker-cap.js";
|
|
6
6
|
import { decodePathParens } from "./path-decode.js";
|
|
7
7
|
import { DEFAULT_LOOP_FLAGS } from "./scheme-types.js";
|
|
8
8
|
import { LineMarkerOps, MimetypeBinary } from "../content/index.js";
|
|
9
9
|
import SchemeCtxImpl from "./caps/SchemeCtxImpl.js";
|
|
10
10
|
import EntryCrud from "../schemes/_entry-crud.js";
|
|
11
|
-
import
|
|
11
|
+
import WorkspaceSettings from "./workspace-settings.js";
|
|
12
12
|
// SPEC §scheme-surface: writer must be in target scheme's manifest.writableBy.
|
|
13
13
|
// OPEN/FOLD/READ/FIND are not gated — they curate the log or read, never mutating an entry.
|
|
14
14
|
const MUTATING_OPS = new Set(["EDIT", "SEND", "COPY", "MOVE", "EXEC", "KILL", "FORK", "WORK"]);
|
|
@@ -32,17 +32,17 @@ class Dispatcher {
|
|
|
32
32
|
// Per-loop abort signal, owned by Engine.runLoop — thunked.
|
|
33
33
|
#loopSignal;
|
|
34
34
|
#streamEventNotify;
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
35
|
+
#wakeWorkerNotify;
|
|
36
|
+
#injectWorker;
|
|
37
|
+
#cancelWorker;
|
|
38
38
|
// §send-premature-terminate/[102]<T> — the engine-owned park-deadline registry (loopId → seconds;
|
|
39
39
|
// -1 = indefinite). The dispatcher WRITES at park; the daemon's drain park-exit consumes.
|
|
40
40
|
#parkDeadlines;
|
|
41
41
|
#searchGate;
|
|
42
|
-
// §join-blocking-collect (#354) — loops with a READ(
|
|
42
|
+
// §join-blocking-collect (#354) — loops with a READ(worker://running-child) armed this turn; a bare
|
|
43
43
|
// SEND[102] parks on it (blocking join). Engine-owned, twin of #parkDeadlines.
|
|
44
44
|
#joinTargets;
|
|
45
|
-
constructor({ db, schemes, mimetypes, tokenize, telemetry, proposals, executors, loopSignal, streamEventNotify,
|
|
45
|
+
constructor({ db, schemes, mimetypes, tokenize, telemetry, proposals, executors, loopSignal, streamEventNotify, wakeWorkerNotify, injectWorker, cancelWorker, searchGate, parkDeadlines, joinTargets }) {
|
|
46
46
|
this.#db = db;
|
|
47
47
|
this.#schemes = schemes;
|
|
48
48
|
this.#mimetypes = mimetypes;
|
|
@@ -52,16 +52,16 @@ class Dispatcher {
|
|
|
52
52
|
this.#executors = executors;
|
|
53
53
|
this.#loopSignal = loopSignal;
|
|
54
54
|
this.#streamEventNotify = streamEventNotify;
|
|
55
|
-
this.#
|
|
56
|
-
this.#
|
|
57
|
-
this.#
|
|
55
|
+
this.#wakeWorkerNotify = wakeWorkerNotify;
|
|
56
|
+
this.#injectWorker = injectWorker;
|
|
57
|
+
this.#cancelWorker = cancelWorker;
|
|
58
58
|
this.#parkDeadlines = parkDeadlines ?? new Map();
|
|
59
59
|
this.#searchGate = searchGate;
|
|
60
60
|
this.#joinTargets = joinTargets ?? new Set();
|
|
61
61
|
}
|
|
62
62
|
async dispatch(context) {
|
|
63
|
-
const { statement,
|
|
64
|
-
const schemeCtx = this.#buildSchemeCtx({
|
|
63
|
+
const { statement, workspaceId, workerId, loopId, turnId, sequence, origin, onDispatch, turnParseErrors } = context;
|
|
64
|
+
const schemeCtx = this.#buildSchemeCtx({ workspaceId, workerId, loopId, turnId, origin });
|
|
65
65
|
let result;
|
|
66
66
|
let denial = this.#checkWritable(statement, origin);
|
|
67
67
|
if (denial === null)
|
|
@@ -73,7 +73,7 @@ class Dispatcher {
|
|
|
73
73
|
// READ honors FIND: a glob/folder scope or a matcher fans out to one log row per MATCH
|
|
74
74
|
// (its own writeLogs), returning early. A READ never proposes, so it bypasses the
|
|
75
75
|
// single-row path below. A bare entry, body-less, falls through to the direct read. #286
|
|
76
|
-
return await this.#handleReadFanout(statement, schemeCtx, {
|
|
76
|
+
return await this.#handleReadFanout(statement, schemeCtx, { workerId, loopId, turnId, sequence, origin, onDispatch });
|
|
77
77
|
}
|
|
78
78
|
else {
|
|
79
79
|
// SPEC §scheme-surface + plurnk-schemes#1: action-entry-as-outcome. Scheme-handler
|
|
@@ -83,10 +83,10 @@ class Dispatcher {
|
|
|
83
83
|
// those are system failures.
|
|
84
84
|
try {
|
|
85
85
|
if (statement.op === "SEND" && statement.target === null) {
|
|
86
|
-
result = await this.#handleSendBroadcast(statement, {
|
|
86
|
+
result = await this.#handleSendBroadcast(statement, { workspaceId, workerId, loopId, turnId, turnParseErrors });
|
|
87
87
|
}
|
|
88
88
|
else if (statement.op === "FORK" || statement.op === "WORK") {
|
|
89
|
-
result = await this.#
|
|
89
|
+
result = await this.#handleWorkerControl(statement, schemeCtx);
|
|
90
90
|
}
|
|
91
91
|
else if (statement.op === "COPY") {
|
|
92
92
|
result = await this.#handleCopy(statement, schemeCtx);
|
|
@@ -122,11 +122,11 @@ class Dispatcher {
|
|
|
122
122
|
// recorded (#382 — a curation act with no forensic trace is how run43's task-frame fold
|
|
123
123
|
// stayed invisible), but engine_render_log suppresses them from the packet, so the receipt
|
|
124
124
|
// rents zero model-facing space — the original clutter concern, resolved by hide-not-drop.
|
|
125
|
-
// §join-blocking-collect (#354) — Run.read on a still-running child returns an
|
|
125
|
+
// §join-blocking-collect (#354) — Run.read on a still-running child returns an awaitWorker signal;
|
|
126
126
|
// arm the join so THIS turn's bare SEND[102] parks (the blocking collect) instead of spinning.
|
|
127
|
-
if (typeof result.
|
|
127
|
+
if (typeof result.awaitWorker === "string")
|
|
128
128
|
this.#joinTargets.add(loopId);
|
|
129
|
-
const logEntryId = await this.#writeLog({ statement, result,
|
|
129
|
+
const logEntryId = await this.#writeLog({ statement, result, workerId, loopId, turnId, sequence, origin });
|
|
130
130
|
// §search-gate — register successful searches AFTER #writeLog stamps the runtime
|
|
131
131
|
// entry's coordinate onto result.attrs.pathname (the gate's dedup serves from it).
|
|
132
132
|
if (statement.op === "EXEC" && result.status < 400) {
|
|
@@ -149,7 +149,7 @@ class Dispatcher {
|
|
|
149
149
|
// no human gate, no loop/proposal notification. Accept + apply
|
|
150
150
|
// in-process; the model sees the outcome directly, never a review.
|
|
151
151
|
if (result.attrs?.inline === true) {
|
|
152
|
-
const effective = await this.#proposals.
|
|
152
|
+
const effective = await this.#proposals.workerApply(statement, result, { decision: "accept" }, { workspaceId, workerId, loopId, turnId });
|
|
153
153
|
return this.#proposals.applyResolution(logEntryId, effective);
|
|
154
154
|
}
|
|
155
155
|
// Register the resolution waiter SYNCHRONOUSLY before any await
|
|
@@ -165,9 +165,9 @@ class Dispatcher {
|
|
|
165
165
|
const flags = await this.#loadLoopFlags(loopId); // the loop/proposal notification carries flags (yolo) — §dual-yolo-proposal-carries-flags
|
|
166
166
|
// #note10 — if the target diverged on disk this turn, the model's EDIT is based
|
|
167
167
|
// on a stale read; flag it so a YOLO auto-accept rejects instead of clobbering.
|
|
168
|
-
const diverged = await this.#db.engine_target_diverged_this_turn.get({
|
|
168
|
+
const diverged = await this.#db.engine_target_diverged_this_turn.get({ worker_id: workerId, turn_id: turnId, scheme: target.scheme, pathname: target.pathname });
|
|
169
169
|
const event = {
|
|
170
|
-
logEntryId,
|
|
170
|
+
logEntryId, workspaceId, workerId, loopId, turnId,
|
|
171
171
|
op: statement.op,
|
|
172
172
|
target: { scheme: target.scheme, pathname: target.pathname },
|
|
173
173
|
body: typeof result.body === "string" ? result.body : "",
|
|
@@ -182,7 +182,7 @@ class Dispatcher {
|
|
|
182
182
|
// 4xx/5xx or throws, the resolution is downgraded to a reject
|
|
183
183
|
// with the failure outcome — engine treats it like a client
|
|
184
184
|
// rejection.
|
|
185
|
-
const effective = await this.#proposals.
|
|
185
|
+
const effective = await this.#proposals.workerApply(statement, result, resolution, { workspaceId, workerId, loopId, turnId });
|
|
186
186
|
// MOVE into a proposed dest: the deferred source-delete fires ONLY now,
|
|
187
187
|
// after the dest write landed (accept). On reject the source survives.
|
|
188
188
|
if (effective.decision === "accept") {
|
|
@@ -209,29 +209,29 @@ class Dispatcher {
|
|
|
209
209
|
// dispatch's resolve path minus #writeLog. Runs on the client loop, so the
|
|
210
210
|
// human's inspection is never constrained by a model loop's flags. {§op-look}
|
|
211
211
|
async look(context) {
|
|
212
|
-
const { statement,
|
|
212
|
+
const { statement, workspaceId, workerId, loopId, origin = "client" } = context;
|
|
213
213
|
if (statement.op !== "READ")
|
|
214
214
|
throw new Error(`look resolves READ only; got ${statement.op}`);
|
|
215
215
|
// turnId is a write-time FK only — a look writes no row, so 0 (no turn) is inert.
|
|
216
|
-
const schemeCtx = this.#buildSchemeCtx({
|
|
216
|
+
const schemeCtx = this.#buildSchemeCtx({ workspaceId, workerId, loopId, turnId: 0, origin });
|
|
217
217
|
const denial = await this.#checkFlagsGate(statement, loopId);
|
|
218
218
|
if (denial !== null)
|
|
219
219
|
return denial;
|
|
220
220
|
return this.#run(schemeNameOf(statement.target), statement, schemeCtx);
|
|
221
221
|
}
|
|
222
222
|
#buildSchemeCtx(ids) {
|
|
223
|
-
const {
|
|
223
|
+
const { workspaceId, workerId, loopId, turnId, origin } = ids;
|
|
224
224
|
return {
|
|
225
225
|
db: this.#db,
|
|
226
|
-
|
|
226
|
+
workspaceId, workerId, loopId, turnId,
|
|
227
227
|
writer: origin,
|
|
228
228
|
signal: this.#loopSignal(loopId),
|
|
229
229
|
streamEventNotify: this.#streamEventNotify,
|
|
230
|
-
|
|
231
|
-
|
|
230
|
+
wakeWorkerNotify: this.#wakeWorkerNotify,
|
|
231
|
+
injectWorker: this.#injectWorker,
|
|
232
232
|
mimetypes: this.#mimetypes,
|
|
233
233
|
tokenize: this.#tokenize,
|
|
234
|
-
pushTelemetry: (event) => this.#telemetry.push(
|
|
234
|
+
pushTelemetry: (event) => this.#telemetry.push(workspaceId, loopId, event),
|
|
235
235
|
executors: this.#executors(),
|
|
236
236
|
};
|
|
237
237
|
}
|
|
@@ -268,11 +268,11 @@ class Dispatcher {
|
|
|
268
268
|
if (statement.op === "EXEC") {
|
|
269
269
|
return this.#denyIfDisallowed("exec", origin);
|
|
270
270
|
}
|
|
271
|
-
// Run control (FORK/WORK →
|
|
271
|
+
// Run control (FORK/WORK → worker://<name>, spawn or fork) is gated by worker://'s writableBy — its
|
|
272
272
|
// body is a seed prompt, not a dst path, so the entry-COPY dst-parse below doesn't apply.
|
|
273
273
|
// §machine-processes
|
|
274
|
-
if (this.#
|
|
275
|
-
return this.#denyIfDisallowed("
|
|
274
|
+
if (this.#isWorkerControl(statement))
|
|
275
|
+
return this.#denyIfDisallowed("worker", origin);
|
|
276
276
|
if (statement.op === "COPY" || statement.op === "MOVE") {
|
|
277
277
|
const dst = statement.op === "COPY" ? (statement.body === null ? null : parsePath(statement.body)) : statement.body;
|
|
278
278
|
const dstScheme = schemeNameOf(dst);
|
|
@@ -302,7 +302,7 @@ class Dispatcher {
|
|
|
302
302
|
const command = ("body" in statement && typeof statement.body === "string") ? statement.body : "";
|
|
303
303
|
const verdict = this.#searchGate?.check(loopId, turnId, runtime, command) ?? { verdict: "pass" };
|
|
304
304
|
if (verdict.verdict === "capped") {
|
|
305
|
-
return { status: 429, error: `Per-turn search limit reached (${verdict.cap})
|
|
305
|
+
return { status: 429, error: `Per-turn search limit reached (${verdict.cap}).` };
|
|
306
306
|
}
|
|
307
307
|
if (verdict.verdict === "duplicate") {
|
|
308
308
|
const prior = await EntryCrud.readEntry(verdict.priorPathname, ctx, runtime);
|
|
@@ -380,7 +380,7 @@ class Dispatcher {
|
|
|
380
380
|
return null;
|
|
381
381
|
return { status: 403, error: `'${scheme}' is unavailable: ${restriction}. Do NOT retry it — it is unavailable, not failing; answer or advise the user directly (an act-mode loop is required to use it).` };
|
|
382
382
|
};
|
|
383
|
-
if (this.#
|
|
383
|
+
if (this.#isWorkerControl(statement))
|
|
384
384
|
return check(statement.target); // body is a spawn/fork task, not a dst path
|
|
385
385
|
if (statement.op === "COPY" || statement.op === "MOVE") {
|
|
386
386
|
return check(statement.target) ?? check(statement.op === "COPY" ? (statement.body === null ? null : parsePath(statement.body)) : statement.body);
|
|
@@ -388,59 +388,59 @@ class Dispatcher {
|
|
|
388
388
|
return check(statement.target);
|
|
389
389
|
}
|
|
390
390
|
// Run control is FORK/WORK (grammar 0.74.55), not COPY — its body
|
|
391
|
-
// is the new
|
|
391
|
+
// is the new worker's seed prompt, not a destination path. The COPY gates and #handleCopy
|
|
392
392
|
// branch on this so they never parse the prompt as a dst path.
|
|
393
|
-
#
|
|
394
|
-
return statement.op === "FORK" || statement.op === "WORK"; // run control targets
|
|
393
|
+
#isWorkerControl(statement) {
|
|
394
|
+
return statement.op === "FORK" || statement.op === "WORK"; // run control targets worker://<name> (grammar 0.74.55)
|
|
395
395
|
}
|
|
396
|
-
// FORK/WORK(
|
|
397
|
-
// •
|
|
396
|
+
// FORK/WORK(worker://<name>):task — run control (grammar 0.74.55):
|
|
397
|
+
// • worker://self → FORK: deep-copy the current worker's log into a new sister (Fork), then
|
|
398
398
|
// continue it with the prompt (§machine-processes-fork-copies-the-log).
|
|
399
|
-
// •
|
|
399
|
+
// • worker://<name> → SPAWN: a fresh sister (empty log) named <name>, started on the prompt.
|
|
400
400
|
// A LIVE sister already holding <name> is a 409 conflict; a free or terminated name is
|
|
401
|
-
// reclaimed (§
|
|
402
|
-
// Both ride the daemon inject and obey the active-runs cap (508, §
|
|
401
|
+
// reclaimed (§worker-scheme-spawn). The self form is fork; only a name spawns.
|
|
402
|
+
// Both ride the daemon inject and obey the active-runs cap (508, §worker-scheme-cap).
|
|
403
403
|
// FORK/WORK — run control (grammar 0.74.55). Both name a NEW run in the target authority
|
|
404
|
-
// (
|
|
405
|
-
// the current
|
|
404
|
+
// (worker://<name>) and carry its seed task in the body. WORK spawns a fresh worker; FORK branches
|
|
405
|
+
// the current worker's log into a named sister. Replaces the COPY(worker://) overload — one verb, one
|
|
406
406
|
// intent, so the model never conflates the target slot with the body (grammar#52).
|
|
407
|
-
async #
|
|
407
|
+
async #handleWorkerControl(statement, ctx) {
|
|
408
408
|
const target = statement.target;
|
|
409
409
|
if (target === null)
|
|
410
|
-
return { status: 400, error: `${statement.op} requires a
|
|
411
|
-
const name = target.kind === "url" ? (target.hostname ?? "") : ""; // §
|
|
410
|
+
return { status: 400, error: `${statement.op} requires a worker target (${statement.op}(worker://<name>))` };
|
|
411
|
+
const name = target.kind === "url" ? (target.hostname ?? "") : ""; // §worker-scheme — run is the AUTHORITY (worker://<name>), not the path
|
|
412
412
|
if (name === "")
|
|
413
|
-
return { status: 400, error: `${statement.op} requires a
|
|
413
|
+
return { status: 400, error: `${statement.op} requires a worker name (worker://<name>)` };
|
|
414
414
|
if (name === "self")
|
|
415
|
-
return { status: 400, error: `'self' is the current run — ${statement.op} names a NEW run (
|
|
416
|
-
if (ctx.
|
|
417
|
-
throw new Error("run control:
|
|
418
|
-
const denied = await
|
|
415
|
+
return { status: 400, error: `'self' is the current run — ${statement.op} names a NEW run (worker://<name>)` };
|
|
416
|
+
if (ctx.injectWorker === undefined)
|
|
417
|
+
throw new Error("run control: injectWorker capability absent");
|
|
418
|
+
const denied = await WorkerCap.deny(this.#db, ctx.workspaceId);
|
|
419
419
|
if (denied !== null)
|
|
420
420
|
return denied;
|
|
421
421
|
const prompt = typeof statement.body === "string" ? statement.body : "";
|
|
422
|
-
// §
|
|
422
|
+
// §worker-delegation-inherits-flags — authority flows down the delegation edge: the child's live
|
|
423
423
|
// loop runs with ITS DELEGATOR'S flags. A flagless (non-YOLO) child's every side-effecting op
|
|
424
424
|
// proposes into a resolver-less void — 300s auto-cancel per attempt was the fan-out wedge.
|
|
425
425
|
const flags = await this.#loadLoopFlags(ctx.loopId);
|
|
426
|
-
// A name is frozen per
|
|
426
|
+
// A name is frozen per worker but reclaimable across time (§machine-processes-worker-origin): a LIVE
|
|
427
427
|
// sister holding it is a 409 (legible, never a raw UNIQUE 500); a free/terminated name reclaims.
|
|
428
|
-
const live = await this.#db.
|
|
428
|
+
const live = await this.#db.worker_live_by_name.get({ workspace_id: ctx.workspaceId, name });
|
|
429
429
|
if (live !== undefined)
|
|
430
|
-
return { status: 409, error: `
|
|
430
|
+
return { status: 409, error: `worker '${name}' is already running` };
|
|
431
431
|
if (statement.op === "FORK") {
|
|
432
|
-
// Branch the current
|
|
433
|
-
const
|
|
434
|
-
await ctx.
|
|
432
|
+
// Branch the current worker's log into a named sister.
|
|
433
|
+
const branchWorkerId = await Fork.fork(this.#db, ctx.workerId, name);
|
|
434
|
+
await ctx.injectWorker({ workspaceId: ctx.workspaceId, workerId: branchWorkerId, prompt, flags });
|
|
435
435
|
return { status: 200, body: name };
|
|
436
436
|
}
|
|
437
437
|
// WORK — a fresh worker sister named <name>.
|
|
438
|
-
const row = await this.#db.
|
|
439
|
-
|
|
438
|
+
const row = await this.#db.fork_insert_worker.get({
|
|
439
|
+
workspace_id: ctx.workspaceId, name, parent_worker_id: ctx.workerId, origin: ctx.writer,
|
|
440
440
|
});
|
|
441
441
|
if (row === undefined)
|
|
442
442
|
throw new Error("run spawn: run insert returned no row");
|
|
443
|
-
await ctx.
|
|
443
|
+
await ctx.injectWorker({ workspaceId: ctx.workspaceId, workerId: row.id, prompt, flags });
|
|
444
444
|
return { status: 200, body: name };
|
|
445
445
|
}
|
|
446
446
|
async #handleCopy(statement, ctx) {
|
|
@@ -518,37 +518,37 @@ class Dispatcher {
|
|
|
518
518
|
if (killable !== undefined && typeof killable.kill === "function") {
|
|
519
519
|
return await killable.kill(pathnameFromPath(path), statement.signal, ctx);
|
|
520
520
|
}
|
|
521
|
-
if (schemeName === "
|
|
522
|
-
// Entry-path present → KILL a
|
|
521
|
+
if (schemeName === "worker") {
|
|
522
|
+
// Entry-path present → KILL a worker-scope scratch ENTRY (delete it), self-only —
|
|
523
523
|
// NOT run cancellation. The authority (hostname) names the owner, the pathname the
|
|
524
|
-
// entry; only the path-ABSENT form (
|
|
524
|
+
// entry; only the path-ABSENT form (worker://<name>) terminates the run-as-actor. §worker-scheme
|
|
525
525
|
const entryPath = path.kind === "url" ? (path.pathname ?? "") : "";
|
|
526
526
|
if (entryPath !== "" && entryPath !== "/") {
|
|
527
|
-
const
|
|
528
|
-
return await
|
|
527
|
+
const workerHandler = this.#schemes.get("worker");
|
|
528
|
+
return await workerHandler.deleteEntry(statement, ctx);
|
|
529
529
|
}
|
|
530
|
-
// terminate — abort any
|
|
531
|
-
// `
|
|
530
|
+
// terminate — abort any worker by address; whoever holds it may end it.
|
|
531
|
+
// `worker://self` = self. cancelWorker (→ Daemon.cancelDrain) aborts the worker's signal
|
|
532
532
|
// (its loop closes 499); an idle run is a no-op-200, a missing run 404.
|
|
533
|
-
const name = path.kind === "url" ? (path.hostname ?? "") : ""; // §
|
|
533
|
+
const name = path.kind === "url" ? (path.hostname ?? "") : ""; // §worker-scheme — run is the AUTHORITY
|
|
534
534
|
if (name === "")
|
|
535
|
-
return { status: 400, error: "
|
|
536
|
-
let
|
|
535
|
+
return { status: 400, error: "worker:// kill requires a worker name or 'self' (worker://<name>)" };
|
|
536
|
+
let workerId = ctx.workerId;
|
|
537
537
|
if (name !== "self") {
|
|
538
|
-
const row = await this.#db.
|
|
538
|
+
const row = await this.#db.worker_resolve_by_name.get({ workspace_id: ctx.workspaceId, name });
|
|
539
539
|
if (row === undefined)
|
|
540
|
-
return { status: 404, error: `
|
|
541
|
-
|
|
540
|
+
return { status: 404, error: `worker://${name} not found in this workspace` };
|
|
541
|
+
workerId = row.id;
|
|
542
542
|
}
|
|
543
|
-
if (this.#
|
|
544
|
-
throw new Error("run kill:
|
|
545
|
-
// §op-synchronous — KILL is DECISIVE: flip the
|
|
543
|
+
if (this.#cancelWorker === undefined)
|
|
544
|
+
throw new Error("run kill: cancelWorker capability absent");
|
|
545
|
+
// §op-synchronous — KILL is DECISIVE: flip the worker's live loops to 499 NOW so the
|
|
546
546
|
// same-turn premature-terminate gate sees it dead (KILL … SEND[200] concludes in ONE
|
|
547
547
|
// turn — the model never reasons about async reap timing). The physical scope reap
|
|
548
|
-
// (drain abort + stream teardown) then rides
|
|
549
|
-
// because
|
|
550
|
-
await this.#db.
|
|
551
|
-
this.#
|
|
548
|
+
// (drain abort + stream teardown) then rides cancelWorker; a killed loop can't heal back
|
|
549
|
+
// because cancelWorker aborts its drain's signal.
|
|
550
|
+
await this.#db.engine_terminate_worker_live_loops.run({ worker_id: workerId, message: "killed via worker:// KILL" });
|
|
551
|
+
this.#cancelWorker(workerId);
|
|
552
552
|
return { status: 200 };
|
|
553
553
|
}
|
|
554
554
|
const handler = this.#schemes.get(schemeName);
|
|
@@ -589,7 +589,7 @@ class Dispatcher {
|
|
|
589
589
|
return { ...statement, target: target, lineMarker, body: null };
|
|
590
590
|
}
|
|
591
591
|
async #handleReadFanout(statement, ctx, ids) {
|
|
592
|
-
const {
|
|
592
|
+
const { workerId, loopId, turnId, sequence, origin, onDispatch } = ids;
|
|
593
593
|
const schemeName = schemeNameOf(statement.target);
|
|
594
594
|
const found = await this.#run(schemeName, { ...statement, op: "FIND" }, ctx);
|
|
595
595
|
const matches = found.matches ?? [];
|
|
@@ -597,7 +597,7 @@ class Dispatcher {
|
|
|
597
597
|
// status, exactly like a non-fanned READ. The model sees the empty/failed result, not silence.
|
|
598
598
|
if (found.status !== 200 || matches.length === 0) {
|
|
599
599
|
const result = { status: found.status === 200 ? 204 : found.status };
|
|
600
|
-
const id = await this.#writeLog({ statement, result,
|
|
600
|
+
const id = await this.#writeLog({ statement, result, workerId, loopId, turnId, sequence, origin });
|
|
601
601
|
onDispatch?.(id);
|
|
602
602
|
return { ...result, rowsWritten: 1 };
|
|
603
603
|
}
|
|
@@ -611,7 +611,7 @@ class Dispatcher {
|
|
|
611
611
|
// had FINDed then READ. On a degenerate single-line document the N delivery rows are
|
|
612
612
|
// identical whole-file lines; the summary row is what tells the model its query hit N
|
|
613
613
|
// times and WHERE (run30: two hits indistinguishable from failure; 17 retries, 508).
|
|
614
|
-
const findRowId = await this.#writeLog({ statement: { ...statement, op: "FIND" }, result: found,
|
|
614
|
+
const findRowId = await this.#writeLog({ statement: { ...statement, op: "FIND" }, result: found, workerId, loopId, turnId, sequence, origin });
|
|
615
615
|
onDispatch?.(findRowId);
|
|
616
616
|
const wholeByPath = new Map();
|
|
617
617
|
const fannedStatuses = [];
|
|
@@ -623,7 +623,7 @@ class Dispatcher {
|
|
|
623
623
|
wholeByPath.set(m.pathname, whole);
|
|
624
624
|
}
|
|
625
625
|
const result = _a.#sliceMatch(whole, m.span);
|
|
626
|
-
const id = await this.#writeLog({ statement: _a.#retargetRead(statement, m.pathname, m.span), result,
|
|
626
|
+
const id = await this.#writeLog({ statement: _a.#retargetRead(statement, m.pathname, m.span), result, workerId, loopId, turnId, sequence: sequence + written, origin });
|
|
627
627
|
onDispatch?.(id);
|
|
628
628
|
fannedStatuses.push(result.status);
|
|
629
629
|
written++;
|
|
@@ -645,16 +645,17 @@ class Dispatcher {
|
|
|
645
645
|
// the model can finally SEE its own prior output (and reason through its own syntax errors).
|
|
646
646
|
// Born FOLDED by default (budget-neutral until OPENed); the turn-0 exemplar passes folded:false
|
|
647
647
|
// (born open — the one worked example the model orients on, thinning the grammar). text/vnd.plurnk.
|
|
648
|
-
async writeModelEntry({ verbatim,
|
|
648
|
+
async writeModelEntry({ verbatim, workerId, loopId, turnId, sequence, folded, origin = "model", reasoningEncrypted }) {
|
|
649
649
|
const row = await this.#db.engine_insert_log_entry.get({
|
|
650
|
-
|
|
650
|
+
worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence,
|
|
651
651
|
origin, source: null, op: "model", suffix: "", signal: null,
|
|
652
652
|
scheme: null, username: null, password: null, hostname: null, port: null,
|
|
653
653
|
pathname: null, params: null, fragment: null, lineMarker: null,
|
|
654
654
|
tx: "", mimetype_tx: "text/vnd.plurnk",
|
|
655
655
|
rx: JSON.stringify({ content: verbatim, mimetype: "text/vnd.plurnk" }),
|
|
656
656
|
mimetype_rx: "application/json",
|
|
657
|
-
status_rx: 200, tokens: this.#tokenize(verbatim), state: "resolved", outcome: null,
|
|
657
|
+
status_rx: 200, tokens: this.#tokenize(verbatim), state: "resolved", outcome: null,
|
|
658
|
+
attrs: reasoningEncrypted !== undefined && reasoningEncrypted.length > 0 ? JSON.stringify({ reasoningEncrypted }) : "{}",
|
|
658
659
|
});
|
|
659
660
|
if (row === undefined)
|
|
660
661
|
throw new Error("Dispatcher.writeModelEntry: insert returned no row");
|
|
@@ -754,41 +755,41 @@ class Dispatcher {
|
|
|
754
755
|
// pending = open streams ∪ live children ∪ THIS turn's retrievals (READ/FIND/OPEN, results unseen
|
|
755
756
|
// until next packet). One rule, one steer, one repair family: nothing pending may be silently
|
|
756
757
|
// discarded; 499 discards BY STATED INTENT and is never gated.
|
|
757
|
-
async #pendingSet(
|
|
758
|
+
async #pendingSet(workerId, turnId) {
|
|
758
759
|
const pending = [];
|
|
759
|
-
const openSubs = await this.#db.
|
|
760
|
+
const openSubs = await this.#db.find_open_subscriptions_for_worker.all({ worker_id: workerId });
|
|
760
761
|
const execHandler = this.#schemes.get("exec");
|
|
761
|
-
if (openSubs.length > 0 || execHandler?.hasActiveSpawns?.(
|
|
762
|
-
pending.push("
|
|
763
|
-
const liveChild = await this.#db.
|
|
762
|
+
if (openSubs.length > 0 || execHandler?.hasActiveSpawns?.(workerId) === true)
|
|
763
|
+
pending.push("surviving streams");
|
|
764
|
+
const liveChild = await this.#db.engine_worker_has_live_child.get({ worker_id: workerId });
|
|
764
765
|
if (liveChild !== undefined)
|
|
765
|
-
pending.push("
|
|
766
|
+
pending.push("surviving workers");
|
|
766
767
|
const retrievals = await this.#db.engine_turn_retrievals.all({ turn_id: turnId });
|
|
767
768
|
if (retrievals.length > 0)
|
|
768
|
-
pending.push("
|
|
769
|
+
pending.push("this turn's retrieval results (they land in the NEXT packet's Log)");
|
|
769
770
|
// §send-undelivered-child-term — a worker that concluded DURING this turn's generation is no
|
|
770
771
|
// longer "live" but its deliverable hasn't reached any packet yet; concluding discards it.
|
|
771
|
-
const undelivered = await this.#db.
|
|
772
|
+
const undelivered = await this.#db.engine_worker_has_undelivered_child_term.get({ worker_id: workerId, turn_id: turnId });
|
|
772
773
|
if (undelivered !== undefined)
|
|
773
774
|
pending.push("worker results that arrived during this turn (they land NEXT turn)");
|
|
774
775
|
return pending;
|
|
775
776
|
}
|
|
776
777
|
// J — a live obligation to WAIT on: a spawned child or an open stream (NOT retrievals, which land
|
|
777
778
|
// next turn regardless). The wait-side twin of #pendingSet's stream+child legs (§wait-obligation-matrix).
|
|
778
|
-
async #hasLiveWork(
|
|
779
|
-
const openSubs = await this.#db.
|
|
779
|
+
async #hasLiveWork(workerId) {
|
|
780
|
+
const openSubs = await this.#db.find_open_subscriptions_for_worker.all({ worker_id: workerId });
|
|
780
781
|
if (openSubs.length > 0)
|
|
781
782
|
return true;
|
|
782
783
|
const execHandler = this.#schemes.get("exec");
|
|
783
|
-
if (execHandler?.hasActiveSpawns?.(
|
|
784
|
+
if (execHandler?.hasActiveSpawns?.(workerId) === true)
|
|
784
785
|
return true;
|
|
785
|
-
const liveChild = await this.#db.
|
|
786
|
+
const liveChild = await this.#db.engine_worker_has_live_child.get({ worker_id: workerId });
|
|
786
787
|
return liveChild !== undefined;
|
|
787
788
|
}
|
|
788
789
|
async #handleSendBroadcast(statement, ctx) {
|
|
789
790
|
if (statement.op !== "SEND")
|
|
790
791
|
throw new Error("unreachable");
|
|
791
|
-
const {
|
|
792
|
+
const { workerId, loopId, turnId } = ctx;
|
|
792
793
|
const status = statement.signal;
|
|
793
794
|
if (status === null)
|
|
794
795
|
return { status: 400 };
|
|
@@ -799,7 +800,7 @@ class Dispatcher {
|
|
|
799
800
|
// indefinitely (the butler/worker pattern — owner-ruled ungated; the instrumentation renders
|
|
800
801
|
// it legibly). Internally the parked state remains loops.status=202: the model-facing SIGNAL
|
|
801
802
|
// retired, the engine's park state did not.
|
|
802
|
-
// §join-blocking-collect (#354) — a READ(
|
|
803
|
+
// §join-blocking-collect (#354) — a READ(worker://running-child) this turn armed a join. A BARE
|
|
803
804
|
// continue becomes an indefinite PARK: the blocking collect, on the same park machinery <-1>
|
|
804
805
|
// uses. The armed READ said "I want this result"; parking IS the collect (the model never had
|
|
805
806
|
// to know the park syntax — the engine holds the join). Any SEND clears the per-turn arm; a
|
|
@@ -813,13 +814,13 @@ class Dispatcher {
|
|
|
813
814
|
// §wait-obligation-matrix — the WAIT: SEND[202], and the legacy SEND[102]<T> the terminal
|
|
814
815
|
// redesign spelled the same park with, are one obligation-checked wait (waitpid). A live
|
|
815
816
|
// obligation (a spawned child or open stream, J) BLOCKS the loop until it concludes and
|
|
816
|
-
// reawakens it (§
|
|
817
|
+
// reawakens it (§worker-lifecycle-child-wake); a wait on nothing (∅) is already satisfied and
|
|
817
818
|
// resolves like 200, so <-1>+∅ self-resolves rather than hang the agent; a pending own
|
|
818
819
|
// retrieval (R) just lands next turn, so the wait continues.
|
|
819
820
|
if (status === 202 || (status === 102 && statement.lineMarker !== null)) {
|
|
820
821
|
const marks = statement.lineMarker?.marks[0];
|
|
821
822
|
const seconds = typeof marks === "number" ? marks : -1; // bare 202 / absent T = indefinite, bounded by the join
|
|
822
|
-
if (await this.#hasLiveWork(
|
|
823
|
+
if (await this.#hasLiveWork(workerId)) {
|
|
823
824
|
await this.#db.engine_loop_set_status.run({ status: 202, loop_id: loopId, message: raw.length > 0 ? raw : "waiting on live work" });
|
|
824
825
|
this.#parkDeadlines.set(loopId, seconds);
|
|
825
826
|
return { status: 202, attrs: { waiting: seconds } };
|
|
@@ -832,26 +833,24 @@ class Dispatcher {
|
|
|
832
833
|
// NEXT build. The wait is NOT on nothing — the results are on the doorstep. Parking
|
|
833
834
|
// would hang (the wake edges already fired into an unparked run); R semantics: continue,
|
|
834
835
|
// the deltas land next turn, the model weighs them and concludes.
|
|
835
|
-
const undelivered = await this.#db.
|
|
836
|
+
const undelivered = await this.#db.engine_worker_has_undelivered_child_term.get({ worker_id: workerId, turn_id: turnId });
|
|
836
837
|
if (undelivered !== undefined)
|
|
837
838
|
return { status: 102 };
|
|
838
|
-
// ∅ — a wait on
|
|
839
|
-
// #
|
|
840
|
-
//
|
|
841
|
-
//
|
|
842
|
-
|
|
843
|
-
await this.#db.engine_loop_mark_terminated_by.run({ terminated_by: "collapse", loop_id: loopId });
|
|
844
|
-
return { status: 200 };
|
|
839
|
+
// ∅ — a wait on nothing is a contradiction, the mirror of premature-terminate (owner
|
|
840
|
+
// ruling, #502 run113): concluding it silently laundered the one signal that something
|
|
841
|
+
// was structurally off (the model believed work was pending; nothing was). 409 gives
|
|
842
|
+
// the turn back with the fact — the recovery rail this exact confusion needs.
|
|
843
|
+
return { status: 409, error: "Attempted [202] wait with nothing pending: no surviving streams, no worker runs, no unretrieved results." };
|
|
845
844
|
}
|
|
846
845
|
// §send-300-choices — ask the operator and park (the answer returns via loop.inject).
|
|
847
846
|
// The three-state cascade (owner design): PLURNK_QUESTIONS unset = ALLOWED (not enabled);
|
|
848
847
|
// =0 = DENIED servicewide (a ceiling the client cannot override); ENABLED requires the
|
|
849
|
-
// client to affirmatively pass it per
|
|
850
|
-
// with a human enables its own
|
|
848
|
+
// client to affirmatively pass it per workspace (settings.questions — the interactive client
|
|
849
|
+
// with a human enables its own workspaces; headless/bench never asks). Enabled workspaces ALSO
|
|
851
850
|
// get the questions.md teaching injected (docEntries) — capability and teaching gate as one.
|
|
852
851
|
// Disabled → refused with a self-decide steer, never a park into the void.
|
|
853
852
|
if (status === 300) {
|
|
854
|
-
if (!(await
|
|
853
|
+
if (!(await WorkspaceSettings.questionsEnabled(this.#db, ctx.workspaceId))) {
|
|
855
854
|
return { status: 409, error: "Operator asks ([300]) are not enabled in this environment — no one is watching to answer. Decide yourself and proceed: SEND[102] to continue, or [200] to conclude." };
|
|
856
855
|
}
|
|
857
856
|
// Owner ruling (#346): the question rides the SAME proposal system as file edits and
|
|
@@ -879,12 +878,12 @@ class Dispatcher {
|
|
|
879
878
|
if (failCount > 0) {
|
|
880
879
|
return { status: 409, error: `Termination attempted despite ${failCount} failed operation(s) this turn. The errors land in your log next turn — weigh them, then conclude (or SEND[499] to abandon).` };
|
|
881
880
|
}
|
|
882
|
-
const pending = await this.#pendingSet(
|
|
881
|
+
const pending = await this.#pendingSet(workerId, turnId);
|
|
883
882
|
if (pending.length > 0) {
|
|
884
883
|
// Kind-specific steer (owner wording, xpath/topo forensics): retrievals-only is
|
|
885
884
|
// gemma's read-and-conclude idiom — no lever to pull, the results simply arrive;
|
|
886
885
|
// KILL/park advice only muddies it. Streams/children keep the remedy steer.
|
|
887
|
-
const retrievalsOnly = pending.every((k) => k.startsWith("
|
|
886
|
+
const retrievalsOnly = pending.every((k) => k.startsWith("this turn's retrieval results"));
|
|
888
887
|
if (retrievalsOnly) {
|
|
889
888
|
// attrs.retrievalOnly — the strike decoupler (owner ruling): atomic-turn-
|
|
890
889
|
// pretrained models pair fetch-and-answer by habit; the refusal teaches,
|
|
@@ -897,7 +896,7 @@ class Dispatcher {
|
|
|
897
896
|
// that re-armed this gate four times while she held the correct answer).
|
|
898
897
|
return { status: 409, error: "Last turn both performed retrieval operations and attempted to terminate. Retrieval operations force an additional turn to receive results for review and reaction. To conclude, only use PLAN and SEND[200] operations.", attrs: { retrievalOnly: true } };
|
|
899
898
|
}
|
|
900
|
-
return { status: 409, error: `Attempted [200] termination
|
|
899
|
+
return { status: 409, error: `Attempted [200] termination in a turn that performs retrieval or has surviving work: ${pending.join("; ")}. KILL what you no longer need; SEND[102] (or [102]<seconds>) to receive the rest; then conclude.` };
|
|
901
900
|
}
|
|
902
901
|
await this.#db.engine_loop_set_status.run({ status: 200, loop_id: loopId, message: raw === "" ? null : raw });
|
|
903
902
|
return { status: 200 };
|
|
@@ -944,7 +943,7 @@ class Dispatcher {
|
|
|
944
943
|
return true;
|
|
945
944
|
return !(statement.op === "SEND" && statement.target === null);
|
|
946
945
|
}
|
|
947
|
-
async #writeLog({ statement, result,
|
|
946
|
+
async #writeLog({ statement, result, workerId, loopId, turnId, sequence, origin, }) {
|
|
948
947
|
const target = this.#extractTarget(statement.target);
|
|
949
948
|
const lineMarkerJson = "lineMarker" in statement && statement.lineMarker !== null
|
|
950
949
|
? JSON.stringify(statement.lineMarker)
|
|
@@ -977,7 +976,7 @@ class Dispatcher {
|
|
|
977
976
|
attrsObj.stream = `${runtime}://${coordPathname}`;
|
|
978
977
|
// Mutate the in-memory result.attrs too: the dispatch path
|
|
979
978
|
// hands originalResult.attrs to handler.applyResolution after
|
|
980
|
-
// proposal accept (see ProposalLifecycle.
|
|
979
|
+
// proposal accept (see ProposalLifecycle.workerApply). Both views —
|
|
981
980
|
// the stored row AND the in-memory proposal — need the same
|
|
982
981
|
// pathname so applyResolution writes the entry at the same URI.
|
|
983
982
|
if (result.attrs !== undefined && result.attrs !== null) {
|
|
@@ -988,7 +987,7 @@ class Dispatcher {
|
|
|
988
987
|
const txJson = JSON.stringify(statement);
|
|
989
988
|
const rxJson = JSON.stringify(result);
|
|
990
989
|
const row = await this.#db.engine_insert_log_entry.get({
|
|
991
|
-
|
|
990
|
+
worker_id: workerId,
|
|
992
991
|
loop_id: loopId,
|
|
993
992
|
turn_id: turnId,
|
|
994
993
|
sequence: sequence,
|
|
@@ -1036,10 +1035,10 @@ class Dispatcher {
|
|
|
1036
1035
|
// Every registered (plurnk-namespace) scheme uses its authority as a namespace segment — fold
|
|
1037
1036
|
// it into the canonical pathname so known://x ≡ known:///x ≡ /x and the log keys identically to
|
|
1038
1037
|
// the entry (/prompt/<run>/<loop>/<N>, /docs/x.md). A foreign web host (http://, unregistered) is NOT a
|
|
1039
|
-
// namespace: keep it in hostname.
|
|
1040
|
-
// run selector (§
|
|
1038
|
+
// namespace: keep it in hostname. worker:// is the one registered EXCEPTION — its authority IS the
|
|
1039
|
+
// run selector (§worker-scheme), and worker://self must stay distinct from worker://name, so Run.ts
|
|
1041
1040
|
// folds the owner into the storage path itself, never here.
|
|
1042
|
-
const foldNs = scheme !== null && scheme !== "
|
|
1041
|
+
const foldNs = scheme !== null && scheme !== "worker" && this.#schemes.has(scheme);
|
|
1043
1042
|
return {
|
|
1044
1043
|
scheme, username: path.username, password: path.password,
|
|
1045
1044
|
hostname: foldNs ? null : path.hostname, port: path.port,
|