@plurnk/plurnk-service 1.3.2 → 1.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.defaults +16 -3
- package/INSTALL.md +7 -3
- package/README.md +9 -0
- package/SPEC.md +82 -34
- package/dist/build-info.json +1 -1
- package/dist/content/edit-receipt.d.ts +35 -0
- package/dist/content/edit-receipt.d.ts.map +1 -0
- package/dist/content/edit-receipt.js +158 -0
- package/dist/content/edit-receipt.js.map +1 -0
- package/dist/content/index.d.ts +2 -0
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +1 -0
- package/dist/content/index.js.map +1 -1
- package/dist/content/line-marker.d.ts +3 -1
- package/dist/content/line-marker.d.ts.map +1 -1
- package/dist/content/line-marker.js +2 -0
- package/dist/content/line-marker.js.map +1 -1
- package/dist/core/ChannelWrite.d.ts +2 -1
- package/dist/core/ChannelWrite.d.ts.map +1 -1
- package/dist/core/ChannelWrite.js +7 -6
- package/dist/core/ChannelWrite.js.map +1 -1
- package/dist/core/CoreSchemeServices.d.ts +31 -0
- package/dist/core/CoreSchemeServices.d.ts.map +1 -0
- package/dist/core/CoreSchemeServices.js +37 -0
- package/dist/core/CoreSchemeServices.js.map +1 -0
- package/dist/core/Db.d.ts +2 -13
- package/dist/core/Db.d.ts.map +1 -1
- package/dist/core/Db.js +0 -5
- package/dist/core/Db.js.map +1 -1
- package/dist/core/Dispatcher.d.ts +7 -3
- package/dist/core/Dispatcher.d.ts.map +1 -1
- package/dist/core/Dispatcher.js +266 -70
- package/dist/core/Dispatcher.js.map +1 -1
- package/dist/core/Engine.d.ts +4 -2
- package/dist/core/Engine.d.ts.map +1 -1
- package/dist/core/Engine.js +55 -37
- package/dist/core/Engine.js.map +1 -1
- package/dist/core/Engine.sql +26 -40
- package/dist/core/LiveSubscriptions.d.ts +8 -0
- package/dist/core/LiveSubscriptions.d.ts.map +1 -0
- package/dist/core/LiveSubscriptions.js +32 -0
- package/dist/core/LiveSubscriptions.js.map +1 -0
- package/dist/core/LoopLifecycle.d.ts +20 -0
- package/dist/core/LoopLifecycle.d.ts.map +1 -0
- package/dist/core/LoopLifecycle.js +56 -0
- package/dist/core/LoopLifecycle.js.map +1 -0
- package/dist/core/LoopLifecycle.sql +60 -0
- package/dist/core/Owner.d.ts.map +1 -1
- package/dist/core/Owner.js.map +1 -1
- package/dist/core/PacketBuilder.d.ts.map +1 -1
- package/dist/core/PacketBuilder.js.map +1 -1
- package/dist/core/ProposalLifecycle.d.ts +4 -1
- package/dist/core/ProposalLifecycle.d.ts.map +1 -1
- package/dist/core/ProposalLifecycle.js +12 -6
- package/dist/core/ProposalLifecycle.js.map +1 -1
- package/dist/core/ProviderInstantiate.d.ts.map +1 -1
- package/dist/core/ProviderInstantiate.js +10 -22
- package/dist/core/ProviderInstantiate.js.map +1 -1
- package/dist/core/SchemeRegistry.d.ts +3 -0
- package/dist/core/SchemeRegistry.d.ts.map +1 -1
- package/dist/core/SchemeRegistry.js +23 -0
- package/dist/core/SchemeRegistry.js.map +1 -1
- package/dist/core/TelemetryChannel.d.ts.map +1 -1
- package/dist/core/TelemetryChannel.js.map +1 -1
- package/dist/core/caps/CapsResolve.d.ts.map +1 -1
- package/dist/core/caps/CapsResolve.js.map +1 -1
- package/dist/core/caps/DbChannelCaps.d.ts.map +1 -1
- package/dist/core/caps/DbChannelCaps.js.map +1 -1
- package/dist/core/caps/DbEntryCaps.d.ts.map +1 -1
- package/dist/core/caps/DbEntryCaps.js +8 -4
- package/dist/core/caps/DbEntryCaps.js.map +1 -1
- package/dist/core/caps/DbSubscriptionCaps.d.ts +3 -2
- package/dist/core/caps/DbSubscriptionCaps.d.ts.map +1 -1
- package/dist/core/caps/DbSubscriptionCaps.js +20 -9
- package/dist/core/caps/DbSubscriptionCaps.js.map +1 -1
- package/dist/core/caps/DbTagCaps.d.ts.map +1 -1
- package/dist/core/caps/DbTagCaps.js.map +1 -1
- package/dist/core/caps/SchemeCtxImpl.d.ts +3 -16
- package/dist/core/caps/SchemeCtxImpl.d.ts.map +1 -1
- package/dist/core/caps/SchemeCtxImpl.js +4 -23
- package/dist/core/caps/SchemeCtxImpl.js.map +1 -1
- package/dist/core/fork.d.ts.map +1 -1
- package/dist/core/fork.js.map +1 -1
- package/dist/core/git-iso.d.ts +3 -0
- package/dist/core/git-iso.d.ts.map +1 -1
- package/dist/core/git-iso.js +35 -4
- package/dist/core/git-iso.js.map +1 -1
- package/dist/core/git-membership.d.ts.map +1 -1
- package/dist/core/git-membership.js.map +1 -1
- package/dist/core/git-state.d.ts.map +1 -1
- package/dist/core/git-state.js +3 -6
- package/dist/core/git-state.js.map +1 -1
- package/dist/core/packet-wire.d.ts.map +1 -1
- package/dist/core/packet-wire.js +37 -11
- package/dist/core/packet-wire.js.map +1 -1
- package/dist/core/run-ops.sql +1 -1
- package/dist/core/scheme-types.d.ts.map +1 -1
- package/dist/core/scheme-types.js +3 -4
- package/dist/core/scheme-types.js.map +1 -1
- package/dist/core/turn-scheduler.d.ts +3 -0
- package/dist/core/turn-scheduler.d.ts.map +1 -0
- package/dist/core/turn-scheduler.js +20 -0
- package/dist/core/turn-scheduler.js.map +1 -0
- package/dist/core/worker-cap.d.ts.map +1 -1
- package/dist/core/worker-cap.js.map +1 -1
- package/dist/core/workspace-settings.d.ts.map +1 -1
- package/dist/core/workspace-settings.js.map +1 -1
- package/dist/core/world-state.d.ts.map +1 -1
- package/dist/core/world-state.js.map +1 -1
- package/dist/digest/Digest.d.ts.map +1 -1
- package/dist/digest/Digest.js +4 -6
- package/dist/digest/Digest.js.map +1 -1
- package/dist/schemes/Exec.d.ts +11 -10
- package/dist/schemes/Exec.d.ts.map +1 -1
- package/dist/schemes/Exec.js +45 -38
- package/dist/schemes/Exec.js.map +1 -1
- package/dist/schemes/ExecOutputScheme.d.ts +8 -6
- package/dist/schemes/ExecOutputScheme.d.ts.map +1 -1
- package/dist/schemes/ExecOutputScheme.js +11 -6
- package/dist/schemes/ExecOutputScheme.js.map +1 -1
- package/dist/schemes/File.d.ts +16 -9
- package/dist/schemes/File.d.ts.map +1 -1
- package/dist/schemes/File.js +75 -26
- package/dist/schemes/File.js.map +1 -1
- package/dist/schemes/Log.d.ts +9 -7
- package/dist/schemes/Log.d.ts.map +1 -1
- package/dist/schemes/Log.js +21 -9
- package/dist/schemes/Log.js.map +1 -1
- package/dist/schemes/Prompt.d.ts +1 -4
- package/dist/schemes/Prompt.d.ts.map +1 -1
- package/dist/schemes/Prompt.js +2 -7
- package/dist/schemes/Prompt.js.map +1 -1
- package/dist/schemes/Skill.d.ts +1 -5
- package/dist/schemes/Skill.d.ts.map +1 -1
- package/dist/schemes/Skill.js +4 -11
- package/dist/schemes/Skill.js.map +1 -1
- package/dist/schemes/Worker.d.ts +15 -10
- package/dist/schemes/Worker.d.ts.map +1 -1
- package/dist/schemes/Worker.js +40 -29
- package/dist/schemes/Worker.js.map +1 -1
- package/dist/schemes/_entry-crud.d.ts.map +1 -1
- package/dist/schemes/_entry-crud.js.map +1 -1
- package/dist/schemes/_entry-find.d.ts +8 -8
- package/dist/schemes/_entry-find.d.ts.map +1 -1
- package/dist/schemes/_entry-find.js +76 -23
- package/dist/schemes/_entry-find.js.map +1 -1
- package/dist/schemes/_entry-find.sql +20 -0
- package/dist/schemes/_entry-graph.d.ts.map +1 -1
- package/dist/schemes/_entry-graph.js.map +1 -1
- package/dist/schemes/_entry-manifest.d.ts.map +1 -1
- package/dist/schemes/_entry-manifest.js.map +1 -1
- package/dist/schemes/_entry-ops.d.ts +3 -2
- package/dist/schemes/_entry-ops.d.ts.map +1 -1
- package/dist/schemes/_entry-ops.js +41 -13
- package/dist/schemes/_entry-ops.js.map +1 -1
- package/dist/schemes/_entry-semantic.d.ts +2 -1
- package/dist/schemes/_entry-semantic.d.ts.map +1 -1
- package/dist/schemes/_entry-semantic.js +15 -4
- package/dist/schemes/_entry-semantic.js.map +1 -1
- package/dist/schemes/_entry-semantic.sql +3 -0
- package/dist/schemes/_entry-send.d.ts.map +1 -1
- package/dist/schemes/_entry-send.js.map +1 -1
- package/dist/server/Daemon.d.ts +9 -1
- package/dist/server/Daemon.d.ts.map +1 -1
- package/dist/server/Daemon.js +161 -74
- package/dist/server/Daemon.js.map +1 -1
- package/dist/server/DaemonLock.d.ts +7 -0
- package/dist/server/DaemonLock.d.ts.map +1 -0
- package/dist/server/DaemonLock.js +85 -0
- package/dist/server/DaemonLock.js.map +1 -0
- package/dist/server/clientTurn.d.ts.map +1 -1
- package/dist/server/clientTurn.js +2 -3
- package/dist/server/clientTurn.js.map +1 -1
- package/dist/server/dispatch-as-plurnk.d.ts.map +1 -1
- package/dist/server/dispatch-as-plurnk.js +20 -2
- package/dist/server/dispatch-as-plurnk.js.map +1 -1
- package/dist/server/drain.sql +4 -8
- package/dist/server/envelope.d.ts.map +1 -1
- package/dist/server/envelope.js +2 -3
- package/dist/server/envelope.js.map +1 -1
- package/dist/server/lifecycle-recovery.sql +68 -0
- package/dist/server/logEntry.d.ts.map +1 -1
- package/dist/server/logEntry.js.map +1 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +61 -40
- package/dist/service.js.map +1 -1
- package/migrations/0000-00-00.01_schema.sql +5 -5
- package/package.json +21 -18
package/dist/core/Dispatcher.js
CHANGED
|
@@ -7,12 +7,14 @@ import WorkerCap from "./worker-cap.js";
|
|
|
7
7
|
import { decodePathParens } from "./path-decode.js";
|
|
8
8
|
import Namespace from "./namespace.js";
|
|
9
9
|
import { DEFAULT_LOOP_FLAGS } from "./scheme-types.js";
|
|
10
|
+
import ChannelWrite from "./ChannelWrite.js";
|
|
10
11
|
import { LineMarkerOps, MimetypeBinary } from "../content/index.js";
|
|
11
12
|
import SchemeCtxImpl from "./caps/SchemeCtxImpl.js";
|
|
12
13
|
import EntryCrud from "../schemes/_entry-crud.js";
|
|
13
14
|
import EntryFind from "../schemes/_entry-find.js";
|
|
14
15
|
import EntryOps from "../schemes/_entry-ops.js";
|
|
15
16
|
import WorkspaceSettings from "./workspace-settings.js";
|
|
17
|
+
import LoopLifecycle from "./LoopLifecycle.js";
|
|
16
18
|
// SPEC §scheme-surface: writer must be in target scheme's manifest.writableBy.
|
|
17
19
|
// OPEN/FOLD/READ/FIND are not gated — they curate the log or read, never mutating an entry.
|
|
18
20
|
const MUTATING_OPS = new Set(["EDIT", "SEND", "COPY", "MOVE", "EXEC", "KILL", "FORK", "WORK"]);
|
|
@@ -39,6 +41,7 @@ class Dispatcher {
|
|
|
39
41
|
#wakeWorkerNotify;
|
|
40
42
|
#injectWorker;
|
|
41
43
|
#cancelWorker;
|
|
44
|
+
#cancelDescendants;
|
|
42
45
|
// §send-premature-terminate/[102]<T> — the engine-owned park-deadline registry (loopId → seconds;
|
|
43
46
|
// -1 = indefinite). The dispatcher WRITES at park; the daemon's drain park-exit consumes.
|
|
44
47
|
#parkDeadlines;
|
|
@@ -46,7 +49,10 @@ class Dispatcher {
|
|
|
46
49
|
// §join-blocking-collect (#354) — loops with a READ(worker://running-child) armed this turn; a bare
|
|
47
50
|
// SEND[102] parks on it (blocking join). Engine-owned, twin of #parkDeadlines.
|
|
48
51
|
#joinTargets;
|
|
49
|
-
|
|
52
|
+
#liveSubscriptions;
|
|
53
|
+
#lifecycle;
|
|
54
|
+
#preparedEdits = new WeakMap();
|
|
55
|
+
constructor({ db, schemes, mimetypes, tokenize, telemetry, proposals, executors, loopSignal, streamEventNotify, wakeWorkerNotify, injectWorker, cancelWorker, cancelDescendants, searchGate, parkDeadlines, joinTargets, liveSubscriptions }) {
|
|
50
56
|
this.#db = db;
|
|
51
57
|
this.#schemes = schemes;
|
|
52
58
|
this.#mimetypes = mimetypes;
|
|
@@ -59,13 +65,58 @@ class Dispatcher {
|
|
|
59
65
|
this.#wakeWorkerNotify = wakeWorkerNotify;
|
|
60
66
|
this.#injectWorker = injectWorker;
|
|
61
67
|
this.#cancelWorker = cancelWorker;
|
|
68
|
+
this.#cancelDescendants = cancelDescendants;
|
|
62
69
|
this.#parkDeadlines = parkDeadlines ?? new Map();
|
|
63
70
|
this.#searchGate = searchGate;
|
|
64
71
|
this.#joinTargets = joinTargets ?? new Set();
|
|
72
|
+
this.#liveSubscriptions = liveSubscriptions;
|
|
73
|
+
this.#lifecycle = new LoopLifecycle(db);
|
|
65
74
|
}
|
|
66
75
|
// workspace → project_root, memoized: {§fs-namespace} fixes the root immutably at
|
|
67
76
|
// workspace creation, so a process-lifetime cache can never go stale.
|
|
68
77
|
#rootCache = new Map();
|
|
78
|
+
#handlerContext(scheme, ctx) {
|
|
79
|
+
const manifest = this.#schemes.manifestFor(scheme);
|
|
80
|
+
return manifest === undefined ? null : new SchemeCtxImpl(ctx, scheme, manifest, this.#liveSubscriptions);
|
|
81
|
+
}
|
|
82
|
+
#entryContext(scheme, ctx) {
|
|
83
|
+
const handlerCtx = this.#handlerContext(scheme, ctx);
|
|
84
|
+
return this.#schemes.manifestFor(scheme)?.category === "data" ? handlerCtx : null;
|
|
85
|
+
}
|
|
86
|
+
async #readEntry(scheme, pathname, ctx) {
|
|
87
|
+
const handler = this.#schemes.get(scheme);
|
|
88
|
+
const handlerCtx = this.#entryContext(scheme, ctx);
|
|
89
|
+
if (typeof handler?.readEntry === "function" && handlerCtx !== null)
|
|
90
|
+
return handler.readEntry(pathname, handlerCtx);
|
|
91
|
+
const caps = handlerCtx?.entries;
|
|
92
|
+
if (caps === undefined)
|
|
93
|
+
return { status: 501, entry: null };
|
|
94
|
+
const result = await caps.read(pathname, scheme === "prompt" ? "worker" : "commons");
|
|
95
|
+
return {
|
|
96
|
+
status: result.status,
|
|
97
|
+
entry: result.entry === null
|
|
98
|
+
? null
|
|
99
|
+
: { channels: { ...result.entry.channels }, tags: [...result.entry.tags] },
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
async #writeEntry(scheme, pathname, entry, ctx) {
|
|
103
|
+
const handler = this.#schemes.get(scheme);
|
|
104
|
+
const handlerCtx = this.#entryContext(scheme, ctx);
|
|
105
|
+
if (typeof handler?.writeEntry === "function" && handlerCtx !== null)
|
|
106
|
+
return handler.writeEntry(pathname, entry, handlerCtx);
|
|
107
|
+
const caps = handlerCtx?.entries;
|
|
108
|
+
if (caps === undefined)
|
|
109
|
+
return { status: 501, created: false, entryId: null };
|
|
110
|
+
return caps.write(pathname, entry);
|
|
111
|
+
}
|
|
112
|
+
async #deleteEntry(scheme, pathname, ctx) {
|
|
113
|
+
const handler = this.#schemes.get(scheme);
|
|
114
|
+
const handlerCtx = this.#entryContext(scheme, ctx);
|
|
115
|
+
if (typeof handler?.deleteEntry === "function" && handlerCtx !== null)
|
|
116
|
+
return handler.deleteEntry(pathname, handlerCtx);
|
|
117
|
+
const caps = handlerCtx?.entries;
|
|
118
|
+
return caps === undefined ? { status: 501 } : caps.delete(pathname);
|
|
119
|
+
}
|
|
69
120
|
async #workspaceRoot(workspaceId) {
|
|
70
121
|
if (this.#rootCache.has(workspaceId))
|
|
71
122
|
return this.#rootCache.get(workspaceId) ?? null;
|
|
@@ -85,7 +136,86 @@ class Dispatcher {
|
|
|
85
136
|
if (key !== null)
|
|
86
137
|
target.pathname = key;
|
|
87
138
|
}
|
|
139
|
+
async prepareEditBatches(statements, context) {
|
|
140
|
+
const { workspaceId, workerId, loopId, turnId, origin } = context;
|
|
141
|
+
const schemeCtx = this.#buildSchemeCtx({ workspaceId, workerId, loopId, turnId, origin });
|
|
142
|
+
const groups = new Map();
|
|
143
|
+
for (const statement of statements) {
|
|
144
|
+
const target = this.#extractTarget(statement.target);
|
|
145
|
+
await this.#canonColumns(target, workspaceId);
|
|
146
|
+
const key = JSON.stringify([
|
|
147
|
+
schemeNameOf(statement.target),
|
|
148
|
+
target.scheme,
|
|
149
|
+
target.hostname,
|
|
150
|
+
target.pathname,
|
|
151
|
+
target.fragment,
|
|
152
|
+
]);
|
|
153
|
+
const group = groups.get(key);
|
|
154
|
+
if (group === undefined)
|
|
155
|
+
groups.set(key, [statement]);
|
|
156
|
+
else
|
|
157
|
+
group.push(statement);
|
|
158
|
+
}
|
|
159
|
+
for (const group of groups.values()) {
|
|
160
|
+
const first = group[0];
|
|
161
|
+
const schemeName = schemeNameOf(first.target);
|
|
162
|
+
let initial;
|
|
163
|
+
let denial = group.map((statement) => this.#checkWritable(statement, origin)).find((result) => result !== null) ?? null;
|
|
164
|
+
if (denial === null) {
|
|
165
|
+
for (const statement of group) {
|
|
166
|
+
denial = await this.#checkFlagsGate(statement, loopId);
|
|
167
|
+
if (denial !== null)
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (denial !== null) {
|
|
172
|
+
initial = denial;
|
|
173
|
+
}
|
|
174
|
+
else if (schemeName === null) {
|
|
175
|
+
initial = { status: 400 };
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
const handler = this.#schemes.get(schemeName);
|
|
179
|
+
const method = handler?.editBatch;
|
|
180
|
+
const manifest = this.#schemes.manifestFor(schemeName);
|
|
181
|
+
if (typeof method !== "function" || manifest === undefined) {
|
|
182
|
+
initial = { status: 501 };
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
try {
|
|
186
|
+
const addressedScheme = first.target?.kind === "url" ? first.target.scheme : schemeName;
|
|
187
|
+
initial = await method.call(handler, group, new SchemeCtxImpl(schemeCtx, addressedScheme ?? schemeName, manifest, this.#liveSubscriptions));
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
initial = { status: 500, error: err instanceof Error ? err.message : String(err) };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
let resolveSettled;
|
|
195
|
+
const settled = new Promise((resolve) => { resolveSettled = resolve; });
|
|
196
|
+
const prepared = {
|
|
197
|
+
first: true,
|
|
198
|
+
index: 0,
|
|
199
|
+
initial,
|
|
200
|
+
settled,
|
|
201
|
+
settle: resolveSettled,
|
|
202
|
+
};
|
|
203
|
+
this.#preparedEdits.set(first, prepared);
|
|
204
|
+
for (const [index, statement] of group.slice(1).entries()) {
|
|
205
|
+
this.#preparedEdits.set(statement, { ...prepared, first: false, index: index + 1 });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
88
209
|
async dispatch(context) {
|
|
210
|
+
const result = await this.#dispatchOne(context);
|
|
211
|
+
if (context.statement.op === "EDIT") {
|
|
212
|
+
const prepared = this.#preparedEdits.get(context.statement);
|
|
213
|
+
if (prepared?.first === true)
|
|
214
|
+
prepared.settle(result);
|
|
215
|
+
}
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
async #dispatchOne(context) {
|
|
89
219
|
const { statement, workspaceId, workerId, loopId, turnId, sequence, origin, onDispatch, turnParseErrors } = context;
|
|
90
220
|
const schemeCtx = this.#buildSchemeCtx({ workspaceId, workerId, loopId, turnId, origin });
|
|
91
221
|
let result;
|
|
@@ -108,7 +238,37 @@ class Dispatcher {
|
|
|
108
238
|
// skips it. Logging failures (#writeLog throws) are NOT caught —
|
|
109
239
|
// those are system failures.
|
|
110
240
|
try {
|
|
111
|
-
if (statement.op === "
|
|
241
|
+
if (statement.op === "EDIT") {
|
|
242
|
+
const prepared = this.#preparedEdits.get(statement);
|
|
243
|
+
if (prepared === undefined) {
|
|
244
|
+
result = { status: 500, error: "EDIT reached dispatch without a prepared resource batch" };
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
const settled = prepared.first ? prepared.initial : await prepared.settled;
|
|
248
|
+
const aggregate = settled.editReceipt ?? prepared.initial.editReceipt;
|
|
249
|
+
if (aggregate !== null && typeof aggregate === "object") {
|
|
250
|
+
const receipt = aggregate;
|
|
251
|
+
const effect = receipt.effects[prepared.index];
|
|
252
|
+
if (effect === undefined)
|
|
253
|
+
throw new Error(`EDIT receipt has no effect at index ${prepared.index}`);
|
|
254
|
+
const { editReceipt: _editReceipt, ...withoutAggregate } = settled;
|
|
255
|
+
result = {
|
|
256
|
+
...withoutAggregate,
|
|
257
|
+
receipt: {
|
|
258
|
+
revision: receipt.revision,
|
|
259
|
+
unit: receipt.unit,
|
|
260
|
+
before: receipt.before,
|
|
261
|
+
after: receipt.after,
|
|
262
|
+
effect,
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
result = settled;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
else if (statement.op === "SEND" && statement.target === null) {
|
|
112
272
|
result = await this.#handleSendBroadcast(statement, { workspaceId, workerId, loopId, turnId, turnParseErrors });
|
|
113
273
|
}
|
|
114
274
|
else if (statement.op === "FORK" || statement.op === "WORK") {
|
|
@@ -216,12 +376,13 @@ class Dispatcher {
|
|
|
216
376
|
const moveSource = result.attrs?.moveSource;
|
|
217
377
|
if (moveSource !== undefined) {
|
|
218
378
|
const srcHandler = this.#schemes.get(moveSource.scheme);
|
|
219
|
-
if (srcHandler !== undefined
|
|
220
|
-
const del = await
|
|
379
|
+
if (srcHandler !== undefined) {
|
|
380
|
+
const del = await this.#deleteEntry(moveSource.scheme, moveSource.pathname, schemeCtx);
|
|
221
381
|
// A host-effecting source-delete PROPOSES (202); the MOVE proposal already gated the whole
|
|
222
382
|
// create+kill, so apply the source-delete now — never raise a second review for one MOVE.
|
|
223
|
-
|
|
224
|
-
|
|
383
|
+
const sourceCtx = this.#handlerContext(moveSource.scheme, schemeCtx);
|
|
384
|
+
if (del.status === 202 && del.attrs !== undefined && typeof srcHandler.applyResolution === "function" && sourceCtx !== null)
|
|
385
|
+
await srcHandler.applyResolution({ attrs: del.attrs }, sourceCtx);
|
|
225
386
|
}
|
|
226
387
|
}
|
|
227
388
|
}
|
|
@@ -501,8 +662,7 @@ class Dispatcher {
|
|
|
501
662
|
const srcSchemeName = schemeNameOf(srcPath);
|
|
502
663
|
if (srcSchemeName === null)
|
|
503
664
|
return { status: 400, error: "MOVE source must be a URL path with a scheme" };
|
|
504
|
-
|
|
505
|
-
if (srcHandler === undefined || typeof srcHandler.deleteEntry !== "function")
|
|
665
|
+
if (!this.#schemes.has(srcSchemeName))
|
|
506
666
|
return { status: 501 };
|
|
507
667
|
// Relocation: COPY then DELETE source (§move-relocation-deletes-source).
|
|
508
668
|
const copyResult = await this.#copyOrchestration({ statement, srcPath, dstPath, ctx });
|
|
@@ -516,7 +676,7 @@ class Dispatcher {
|
|
|
516
676
|
if (copyResult.status === 202) {
|
|
517
677
|
return { ...copyResult, attrs: { ...copyResult.attrs, moveSource: { scheme: srcSchemeName, pathname: srcPathname } } };
|
|
518
678
|
}
|
|
519
|
-
const delResult = await
|
|
679
|
+
const delResult = await this.#deleteEntry(srcSchemeName, srcPathname, ctx);
|
|
520
680
|
if (delResult.status >= 400)
|
|
521
681
|
return { status: delResult.status };
|
|
522
682
|
return copyResult;
|
|
@@ -549,7 +709,8 @@ class Dispatcher {
|
|
|
549
709
|
if (killable !== undefined && typeof killable.kill === "function") {
|
|
550
710
|
// Pass the model's OWN scheme so a stream-KILL error answers in the runtime tag the
|
|
551
711
|
// model addressed (sh:///…), not the internal `exec` ({§fs-answer-in-canon}).
|
|
552
|
-
|
|
712
|
+
const handlerCtx = this.#handlerContext(schemeName, ctx);
|
|
713
|
+
return handlerCtx === null ? { status: 501 } : await killable.kill(pathnameFromPath(path), statement.signal, handlerCtx, schemeName);
|
|
553
714
|
}
|
|
554
715
|
if (schemeName === "worker") {
|
|
555
716
|
// Entry-path present → KILL a worker-scope scratch ENTRY (delete it), self-only —
|
|
@@ -558,11 +719,11 @@ class Dispatcher {
|
|
|
558
719
|
const entryPath = path.kind === "url" ? (path.pathname ?? "") : "";
|
|
559
720
|
if (entryPath !== "" && entryPath !== "/") {
|
|
560
721
|
const workerHandler = this.#schemes.get("worker");
|
|
561
|
-
|
|
722
|
+
const handlerCtx = this.#handlerContext("worker", ctx);
|
|
723
|
+
return handlerCtx === null ? { status: 501 } : await workerHandler.killEntry(statement, handlerCtx);
|
|
562
724
|
}
|
|
563
|
-
//
|
|
564
|
-
//
|
|
565
|
-
// (its loop closes 499); an idle run is a no-op-200, a missing run 404.
|
|
725
|
+
// Terminate an addressed worker's structured scope. `worker://self` = self.
|
|
726
|
+
// An idle worker is a no-op 200; a missing worker is 404.
|
|
566
727
|
const name = path.kind === "url" ? (path.hostname ?? "") : ""; // §worker-scheme — the worker is the AUTHORITY
|
|
567
728
|
if (name === "")
|
|
568
729
|
return { status: 400, error: "worker:// kill requires a worker name or ~ (worker://<name>)" };
|
|
@@ -575,21 +736,16 @@ class Dispatcher {
|
|
|
575
736
|
}
|
|
576
737
|
if (this.#cancelWorker === undefined)
|
|
577
738
|
throw new Error("run kill: cancelWorker capability absent");
|
|
578
|
-
// §op-synchronous — KILL is
|
|
579
|
-
// same-turn
|
|
580
|
-
|
|
581
|
-
// (drain abort + stream teardown) then rides cancelWorker; a killed loop can't heal back
|
|
582
|
-
// because cancelWorker aborts its drain's signal.
|
|
583
|
-
await this.#db.engine_terminate_worker_live_loops.run({ worker_id: workerId, message: "killed via worker:// KILL" });
|
|
584
|
-
this.#cancelWorker(workerId);
|
|
739
|
+
// §op-synchronous — KILL is decisive. Await the one lifecycle owner so the
|
|
740
|
+
// same-turn pending-work gate observes the complete subtree as terminal.
|
|
741
|
+
await this.#cancelWorker(workerId, "killed via worker:// KILL");
|
|
585
742
|
return { status: 200 };
|
|
586
743
|
}
|
|
587
|
-
|
|
588
|
-
if (handler === undefined || typeof handler.deleteEntry !== "function")
|
|
744
|
+
if (!this.#schemes.has(schemeName))
|
|
589
745
|
return { status: 501 };
|
|
590
746
|
// A host-effecting delete (file) returns 202 to PROPOSE — pass its attrs through so the proposal
|
|
591
747
|
// carries the delete target to review (§isProposal fires on 202). Plurnk-internal deletes execute inline.
|
|
592
|
-
const delResult = await
|
|
748
|
+
const delResult = await this.#deleteEntry(schemeName, pathnameFromPath(path), ctx);
|
|
593
749
|
return delResult.attrs !== undefined ? { status: delResult.status, attrs: delResult.attrs } : { status: delResult.status };
|
|
594
750
|
}
|
|
595
751
|
// Multi-file READ fan-out (SPEC §matcher-result — "the companion to FIND's survey"). A glob
|
|
@@ -613,15 +769,15 @@ class Dispatcher {
|
|
|
613
769
|
const schemeName = schemeNameOf(t);
|
|
614
770
|
return p.endsWith("/") && schemeName !== null && this.#schemes.manifestFor(schemeName)?.folderScopes === true;
|
|
615
771
|
}
|
|
616
|
-
// Clone the READ onto one concrete match — the FIND already matched, so the
|
|
617
|
-
//
|
|
618
|
-
//
|
|
772
|
+
// Clone the READ onto one concrete match — the FIND already matched, so strip the body
|
|
773
|
+
// (no re-match) and set <L> to its readable-row span. A null span (body-less folder/glob
|
|
774
|
+
// fan-out) reads the whole entry. #286
|
|
619
775
|
static #retargetRead(statement, pathname, span) {
|
|
620
776
|
const t = statement.target;
|
|
621
777
|
const target = t !== null && t.kind === "url"
|
|
622
778
|
? { ...t, pathname, raw: `${t.scheme}://${pathname}` }
|
|
623
779
|
: { ...t, raw: pathname };
|
|
624
|
-
const lineMarker = span !== null ? { marks: [span.
|
|
780
|
+
const lineMarker = span !== null ? { marks: [span.rowStart, span.rowEnd] } : null;
|
|
625
781
|
return { ...statement, target: target, lineMarker, body: null };
|
|
626
782
|
}
|
|
627
783
|
#standardEntryManifest(schemeName, statement) {
|
|
@@ -652,7 +808,7 @@ class Dispatcher {
|
|
|
652
808
|
const displayPath = hostPrefix !== null && pathname.startsWith(`${hostPrefix}/`)
|
|
653
809
|
? pathname.slice(hostPrefix.length)
|
|
654
810
|
: pathname;
|
|
655
|
-
const lineMarker = span === null ? null : { marks: [span.
|
|
811
|
+
const lineMarker = span === null ? null : { marks: [span.rowStart, span.rowEnd] };
|
|
656
812
|
return {
|
|
657
813
|
...statement,
|
|
658
814
|
target: storage
|
|
@@ -695,10 +851,10 @@ class Dispatcher {
|
|
|
695
851
|
onDispatch?.(id);
|
|
696
852
|
return { ...result, rowsWritten: 1 };
|
|
697
853
|
}
|
|
698
|
-
// One READ row per MATCH —
|
|
699
|
-
// folder/glob)
|
|
700
|
-
//
|
|
701
|
-
// distinct entry's content once, then line-slice per match. #286
|
|
854
|
+
// One READ row per MATCH — deliver its source lines (or the whole entry for a body-less
|
|
855
|
+
// folder/glob) while the logged statement carries the corresponding readable-row scope.
|
|
856
|
+
// The delivery uses a raw line-slice rather than reinterpreting provenance as the scheme's
|
|
857
|
+
// structural <L>. Read each distinct entry's content once, then line-slice per match. #286
|
|
702
858
|
// §matcher-selection-signal — the SELECTION SUMMARY row: the internal FIND that located
|
|
703
859
|
// the matches is WRITTEN (op=FIND, its full result — per-hit matchSpan + matchPath, the
|
|
704
860
|
// canonical dialect coordinate) before the per-match deliveries, exactly as if the model
|
|
@@ -791,20 +947,16 @@ class Dispatcher {
|
|
|
791
947
|
const dstHandler = this.#schemes.get(dstSchemeName);
|
|
792
948
|
if (srcHandler === undefined || dstHandler === undefined)
|
|
793
949
|
return { status: 501 };
|
|
794
|
-
if (typeof srcHandler.readEntry !== "function" || typeof dstHandler.writeEntry !== "function")
|
|
795
|
-
return { status: 501 };
|
|
796
950
|
const srcPathname = pathnameFromPath(srcPath);
|
|
797
951
|
const dstPathname = pathnameFromPath(dstPath);
|
|
798
|
-
const srcResult = await
|
|
952
|
+
const srcResult = await this.#readEntry(srcSchemeName, srcPathname, ctx);
|
|
799
953
|
if (srcResult.status !== 200 || srcResult.entry === null)
|
|
800
954
|
return { status: 404, error: `COPY/MOVE source not found: ${srcSchemeName}://${srcPathname}` }; // §copy-missing-source-404 §move-missing-source-404
|
|
801
955
|
const entry = srcResult.entry;
|
|
802
956
|
// Destination read — the conflict/no-op verdict is deferred until the
|
|
803
957
|
// to-be-written content is known (after <L> slice + tag resolution below),
|
|
804
958
|
// so an identical re-copy resolves to 304 instead of a phantom 409.
|
|
805
|
-
const dstExisting =
|
|
806
|
-
? await dstHandler.readEntry(dstPathname, ctx)
|
|
807
|
-
: null;
|
|
959
|
+
const dstExisting = await this.#readEntry(dstSchemeName, dstPathname, ctx);
|
|
808
960
|
// Mimetype compatibility check against the destination scheme's manifest
|
|
809
961
|
const dstManifest = dstHandler.constructor.manifest;
|
|
810
962
|
const dstChannels = dstManifest?.channels ?? {};
|
|
@@ -852,7 +1004,7 @@ class Dispatcher {
|
|
|
852
1004
|
return { status: 304 }; // identical → §copy-noop-304
|
|
853
1005
|
return { status: 409, error: `COPY/MOVE destination exists: ${dstSchemeName}://${dstPathname}` }; // §copy-conflict-409
|
|
854
1006
|
}
|
|
855
|
-
const writeResult = await
|
|
1007
|
+
const writeResult = await this.#writeEntry(dstSchemeName, dstPathname, { channels, tags }, ctx);
|
|
856
1008
|
// A file dest returns 202 (disk write → §membership review): propagate the
|
|
857
1009
|
// proposal so dispatch runs the gate + routes applyResolution to the dest.
|
|
858
1010
|
if (writeResult.status === 202)
|
|
@@ -874,16 +1026,34 @@ class Dispatcher {
|
|
|
874
1026
|
const liveChild = await this.#db.engine_worker_has_live_child.get({ worker_id: workerId });
|
|
875
1027
|
if (liveChild !== undefined)
|
|
876
1028
|
pending.push("surviving workers");
|
|
877
|
-
const
|
|
878
|
-
if (retrievals
|
|
1029
|
+
const observations = await this.#pendingObservations(workerId, turnId);
|
|
1030
|
+
if (observations.retrievals)
|
|
879
1031
|
pending.push("this turn's retrieval results (they land in the NEXT packet's Log)");
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
if (
|
|
1032
|
+
if (observations.streamTerminations) {
|
|
1033
|
+
pending.push("completed stream results that land in the NEXT packet's Log");
|
|
1034
|
+
}
|
|
1035
|
+
if (observations.childTerminations)
|
|
884
1036
|
pending.push("worker results that arrived during this turn (they land NEXT turn)");
|
|
885
1037
|
return pending;
|
|
886
1038
|
}
|
|
1039
|
+
// Results cross an observation boundary only when they have appeared in a
|
|
1040
|
+
// packet. Completion alone is not delivery. Keep the three next-packet
|
|
1041
|
+
// producers in one classifier so SEND[200]'s discard gate and SEND[202]'s
|
|
1042
|
+
// empty-join decision cannot disagree about what remains unseen.
|
|
1043
|
+
async #pendingObservations(workerId, turnId) {
|
|
1044
|
+
const [retrievals, streamTermination, childTermination] = await Promise.all([
|
|
1045
|
+
this.#db.engine_turn_retrievals.all({ turn_id: turnId }),
|
|
1046
|
+
this.#db.engine_worker_has_undelivered_stream_term
|
|
1047
|
+
.get({ worker_id: workerId }),
|
|
1048
|
+
this.#db.engine_worker_has_undelivered_child_term
|
|
1049
|
+
.get({ worker_id: workerId, turn_id: turnId }),
|
|
1050
|
+
]);
|
|
1051
|
+
return {
|
|
1052
|
+
retrievals: retrievals.length > 0,
|
|
1053
|
+
streamTerminations: streamTermination !== undefined,
|
|
1054
|
+
childTerminations: childTermination !== undefined,
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
887
1057
|
// J — a live obligation to WAIT on: a spawned child or an open stream (NOT retrievals, which land
|
|
888
1058
|
// next turn regardless). The wait-side twin of #pendingSet's stream+child legs (§wait-obligation-matrix).
|
|
889
1059
|
async #hasLiveWork(workerId) {
|
|
@@ -917,12 +1087,14 @@ class Dispatcher {
|
|
|
917
1087
|
// terminal with a live child stays the existing premature-terminate steer (#354 decision #1).
|
|
918
1088
|
const joinArmed = this.#joinTargets.delete(loopId);
|
|
919
1089
|
if (status === 102 && statement.lineMarker === null && joinArmed) {
|
|
920
|
-
await this.#
|
|
1090
|
+
if (!await this.#lifecycle.park(loopId, raw.length > 0 ? raw : "parked — awaiting a worker's result (blocking collect)")) {
|
|
1091
|
+
return { status: await this.#lifecycle.status(loopId) };
|
|
1092
|
+
}
|
|
921
1093
|
this.#parkDeadlines.set(loopId, -1); // indefinite: the bounded child's terminal is the wake edge
|
|
922
1094
|
return { status: 102, attrs: { parked: -1, join: true } };
|
|
923
1095
|
}
|
|
924
|
-
// §wait-obligation-matrix —
|
|
925
|
-
//
|
|
1096
|
+
// §wait-obligation-matrix — SEND[202] and the legacy SEND[102]<T> are one
|
|
1097
|
+
// obligation-checked join. A live
|
|
926
1098
|
// obligation (a spawned child or open stream, J) BLOCKS the loop until it concludes and
|
|
927
1099
|
// reawakens it (§worker-lifecycle-child-wake); a wait on nothing (∅) is already satisfied and
|
|
928
1100
|
// resolves like 200, so <-1>+∅ self-resolves rather than hang the agent; a pending own
|
|
@@ -931,26 +1103,27 @@ class Dispatcher {
|
|
|
931
1103
|
const marks = statement.lineMarker?.marks[0];
|
|
932
1104
|
const seconds = typeof marks === "number" ? marks : -1; // bare 202 / absent T = indefinite, bounded by the join
|
|
933
1105
|
if (await this.#hasLiveWork(workerId)) {
|
|
934
|
-
await this.#
|
|
1106
|
+
if (!await this.#lifecycle.park(loopId, raw.length > 0 ? raw : "waiting on live work")) {
|
|
1107
|
+
return { status: await this.#lifecycle.status(loopId) };
|
|
1108
|
+
}
|
|
935
1109
|
this.#parkDeadlines.set(loopId, seconds);
|
|
936
1110
|
return { status: 202, attrs: { waiting: seconds } };
|
|
937
1111
|
}
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
//
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
// would hang (the wake edges already fired into an unparked run); R semantics: continue,
|
|
945
|
-
// the deltas land next turn, the model weighs them and concludes.
|
|
946
|
-
const undelivered = await this.#db.engine_worker_has_undelivered_child_term.get({ worker_id: workerId, turn_id: turnId });
|
|
947
|
-
if (undelivered !== undefined)
|
|
1112
|
+
// Retrievals, fast stream conclusions, and child conclusions are
|
|
1113
|
+
// all complete-but-unobserved. Their wake edge may already have
|
|
1114
|
+
// fired, so do not park; continue directly to the packet that
|
|
1115
|
+
// materializes them.
|
|
1116
|
+
const observations = await this.#pendingObservations(workerId, turnId);
|
|
1117
|
+
if (observations.retrievals || observations.streamTerminations || observations.childTerminations) {
|
|
948
1118
|
return { status: 102 };
|
|
949
|
-
|
|
950
|
-
//
|
|
951
|
-
//
|
|
952
|
-
|
|
953
|
-
return {
|
|
1119
|
+
}
|
|
1120
|
+
// The joined set is already drained. Awaiting an empty task group completes
|
|
1121
|
+
// immediately; it never parks and needs no corrective model turn.
|
|
1122
|
+
const finished = await this.#lifecycle.finish(loopId, 200, raw === "" ? null : raw);
|
|
1123
|
+
return {
|
|
1124
|
+
status: finished ? 200 : await this.#lifecycle.status(loopId),
|
|
1125
|
+
attrs: { joined: true, pending: 0 },
|
|
1126
|
+
};
|
|
954
1127
|
}
|
|
955
1128
|
// §send-300-choices — ask the operator and park (the answer returns via loop.inject).
|
|
956
1129
|
// The three-state cascade (owner design): PLURNK_QUESTIONS unset = ALLOWED (not enabled);
|
|
@@ -975,7 +1148,7 @@ class Dispatcher {
|
|
|
975
1148
|
}
|
|
976
1149
|
// [200] — terminate, gated by the pending set (post-batch). The row records the refused
|
|
977
1150
|
// attempt faithfully (status_rx=409, never erased); the loop stays a continue; the strike
|
|
978
|
-
// couples in runTurn. [499] abandons
|
|
1151
|
+
// couples in runTurn. [499] abandons and cancels the descendant scope.
|
|
979
1152
|
if (status === 200) {
|
|
980
1153
|
// §send-200-failed-ops (#363, owner ruling: never 200 over a failed op) — the failure
|
|
981
1154
|
// twin of the pending set: this turn's failed op results (and this emission's parse
|
|
@@ -1008,11 +1181,14 @@ class Dispatcher {
|
|
|
1008
1181
|
}
|
|
1009
1182
|
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.` };
|
|
1010
1183
|
}
|
|
1011
|
-
await this.#
|
|
1012
|
-
return { status: 200 };
|
|
1184
|
+
const finished = await this.#lifecycle.finish(loopId, 200, raw === "" ? null : raw);
|
|
1185
|
+
return { status: finished ? 200 : await this.#lifecycle.status(loopId) };
|
|
1013
1186
|
}
|
|
1014
1187
|
if (status === 499) {
|
|
1015
|
-
await this.#
|
|
1188
|
+
const finished = await this.#lifecycle.finish(loopId, 499, raw === "" ? null : raw);
|
|
1189
|
+
if (!finished)
|
|
1190
|
+
return { status: await this.#lifecycle.status(loopId) };
|
|
1191
|
+
await this.#cancelDescendants?.(workerId, raw === "" ? "parent abandoned its scope" : raw);
|
|
1016
1192
|
return { status: 499 };
|
|
1017
1193
|
}
|
|
1018
1194
|
// Every other signal — 102 bare, 202 (retired as a terminal; now ordinary mid-comms), 1xx —
|
|
@@ -1022,6 +1198,26 @@ class Dispatcher {
|
|
|
1022
1198
|
async #run(schemeName, statement, ctx) {
|
|
1023
1199
|
if (schemeName === null)
|
|
1024
1200
|
return { status: 400 };
|
|
1201
|
+
if (statement.op === "SEND" && statement.signal === 499 && statement.target?.kind === "url") {
|
|
1202
|
+
const entry = await this.#db.crud_find_workspace_entry.get({
|
|
1203
|
+
workspace_id: ctx.workspaceId,
|
|
1204
|
+
owner_id: await Owner.commonsId(this.#db, ctx.workspaceId),
|
|
1205
|
+
scheme: schemeName,
|
|
1206
|
+
pathname: pathnameFromPath(statement.target),
|
|
1207
|
+
});
|
|
1208
|
+
if (entry !== undefined) {
|
|
1209
|
+
const subscription = await ChannelWrite.findActiveSubscription(this.#db, {
|
|
1210
|
+
workerId: ctx.workerId,
|
|
1211
|
+
entryId: entry.id,
|
|
1212
|
+
});
|
|
1213
|
+
if (subscription !== null && subscription.scheme === schemeName) {
|
|
1214
|
+
const cancelled = await this.#liveSubscriptions.cancel(subscription.id);
|
|
1215
|
+
return cancelled
|
|
1216
|
+
? { status: 200 }
|
|
1217
|
+
: { status: 500, error: `subscription ${subscription.id} is durable but has no live cancellation handle` };
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1025
1221
|
const handler = this.#schemes.get(schemeName);
|
|
1026
1222
|
if (handler === undefined)
|
|
1027
1223
|
return { status: 501 };
|
|
@@ -1033,7 +1229,7 @@ class Dispatcher {
|
|
|
1033
1229
|
const manifest = this.#schemes.manifestFor(schemeName);
|
|
1034
1230
|
if (manifest === undefined)
|
|
1035
1231
|
throw new Error(`scheme '${schemeName}' has no manifest`);
|
|
1036
|
-
return method.call(handler, statement, new SchemeCtxImpl(ctx, addressedScheme ?? schemeName, manifest));
|
|
1232
|
+
return method.call(handler, statement, new SchemeCtxImpl(ctx, addressedScheme ?? schemeName, manifest, this.#liveSubscriptions));
|
|
1037
1233
|
}
|
|
1038
1234
|
// A status-202 result is a reviewable PROPOSAL (a side-effecting op — EDIT/EXEC/
|
|
1039
1235
|
// directed write — paused for client resolution) UNLESS it is a broadcast SEND.
|