@plurnk/plurnk-service 1.6.0 → 1.7.0

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.
Files changed (164) hide show
  1. package/.env.defaults +7 -21
  2. package/INSTALL.md +18 -17
  3. package/README.md +1 -1
  4. package/SPEC.md +209 -136
  5. package/dist/build-info.json +1 -1
  6. package/dist/core/ChannelWrite.sql +3 -3
  7. package/dist/core/ClientInteractions.d.ts +19 -0
  8. package/dist/core/ClientInteractions.d.ts.map +1 -0
  9. package/dist/core/ClientInteractions.js +124 -0
  10. package/dist/core/ClientInteractions.js.map +1 -0
  11. package/dist/core/ClientInteractions.sql +34 -0
  12. package/dist/core/CoreSchemeServices.d.ts +9 -3
  13. package/dist/core/CoreSchemeServices.d.ts.map +1 -1
  14. package/dist/core/CoreSchemeServices.js +8 -2
  15. package/dist/core/CoreSchemeServices.js.map +1 -1
  16. package/dist/core/Dispatcher.d.ts +4 -2
  17. package/dist/core/Dispatcher.d.ts.map +1 -1
  18. package/dist/core/Dispatcher.js +116 -1149
  19. package/dist/core/Dispatcher.js.map +1 -1
  20. package/dist/core/Engine.d.ts +24 -34
  21. package/dist/core/Engine.d.ts.map +1 -1
  22. package/dist/core/Engine.js +137 -1672
  23. package/dist/core/Engine.js.map +1 -1
  24. package/dist/core/Engine.sql +46 -32
  25. package/dist/core/ExecutableTools.d.ts +1 -0
  26. package/dist/core/ExecutableTools.d.ts.map +1 -1
  27. package/dist/core/ExecutableTools.js +17 -9
  28. package/dist/core/ExecutableTools.js.map +1 -1
  29. package/dist/core/ExecutorRegistry.d.ts +11 -3
  30. package/dist/core/ExecutorRegistry.d.ts.map +1 -1
  31. package/dist/core/ExecutorRegistry.js +139 -16
  32. package/dist/core/ExecutorRegistry.js.map +1 -1
  33. package/dist/core/LogBody.d.ts +1 -0
  34. package/dist/core/LogBody.d.ts.map +1 -1
  35. package/dist/core/LogBody.js +4 -0
  36. package/dist/core/LogBody.js.map +1 -1
  37. package/dist/core/ModelCall.d.ts +2 -2
  38. package/dist/core/ModelCall.d.ts.map +1 -1
  39. package/dist/core/ModelCall.js +4 -2
  40. package/dist/core/ModelCall.js.map +1 -1
  41. package/dist/core/PacketBuilder.d.ts +11 -22
  42. package/dist/core/PacketBuilder.d.ts.map +1 -1
  43. package/dist/core/PacketBuilder.js +90 -180
  44. package/dist/core/PacketBuilder.js.map +1 -1
  45. package/dist/core/ProblemLog.d.ts +1 -1
  46. package/dist/core/ProblemLog.d.ts.map +1 -1
  47. package/dist/core/ProblemLog.js +14 -3
  48. package/dist/core/ProblemLog.js.map +1 -1
  49. package/dist/core/ProposalLifecycle.d.ts +4 -2
  50. package/dist/core/ProposalLifecycle.d.ts.map +1 -1
  51. package/dist/core/ProposalLifecycle.js +19 -7
  52. package/dist/core/ProposalLifecycle.js.map +1 -1
  53. package/dist/core/ProviderInstantiate.js +2 -2
  54. package/dist/core/ProviderInstantiate.js.map +1 -1
  55. package/dist/core/ResourceMutations.d.ts +73 -0
  56. package/dist/core/ResourceMutations.d.ts.map +1 -0
  57. package/dist/core/ResourceMutations.js +1142 -0
  58. package/dist/core/ResourceMutations.js.map +1 -0
  59. package/dist/core/SchemeRegistry.d.ts +18 -10
  60. package/dist/core/SchemeRegistry.d.ts.map +1 -1
  61. package/dist/core/SchemeRegistry.js +195 -43
  62. package/dist/core/SchemeRegistry.js.map +1 -1
  63. package/dist/core/StoredPacket.d.ts +2 -2
  64. package/dist/core/StoredPacket.js +7 -7
  65. package/dist/core/TurnRunner.d.ts +74 -0
  66. package/dist/core/TurnRunner.d.ts.map +1 -0
  67. package/dist/core/TurnRunner.js +1733 -0
  68. package/dist/core/TurnRunner.js.map +1 -0
  69. package/dist/core/WorkspaceGate.d.ts +1 -0
  70. package/dist/core/WorkspaceGate.d.ts.map +1 -1
  71. package/dist/core/WorkspaceGate.js +39 -0
  72. package/dist/core/WorkspaceGate.js.map +1 -1
  73. package/dist/core/caps/CoreInteractionCaps.d.ts +9 -0
  74. package/dist/core/caps/CoreInteractionCaps.d.ts.map +1 -0
  75. package/dist/core/caps/CoreInteractionCaps.js +14 -0
  76. package/dist/core/caps/CoreInteractionCaps.js.map +1 -0
  77. package/dist/core/caps/DbChannelCaps.js +4 -4
  78. package/dist/core/caps/DbChannelCaps.js.map +1 -1
  79. package/dist/core/caps/SchemeCtxImpl.d.ts +2 -1
  80. package/dist/core/caps/SchemeCtxImpl.d.ts.map +1 -1
  81. package/dist/core/caps/SchemeCtxImpl.js +3 -0
  82. package/dist/core/caps/SchemeCtxImpl.js.map +1 -1
  83. package/dist/core/content-hash.js +2 -2
  84. package/dist/core/content-hash.js.map +1 -1
  85. package/dist/core/content-weight.d.ts +2 -0
  86. package/dist/core/content-weight.d.ts.map +1 -0
  87. package/dist/core/content-weight.js +4 -0
  88. package/dist/core/content-weight.js.map +1 -0
  89. package/dist/core/content_weight.d.ts +4 -0
  90. package/dist/core/content_weight.d.ts.map +1 -0
  91. package/dist/core/content_weight.js +6 -0
  92. package/dist/core/content_weight.js.map +1 -0
  93. package/dist/core/fork.sql +5 -5
  94. package/dist/core/packet-wire.d.ts +4 -4
  95. package/dist/core/packet-wire.d.ts.map +1 -1
  96. package/dist/core/packet-wire.js +21 -21
  97. package/dist/core/packet-wire.js.map +1 -1
  98. package/dist/core/scheme-types.d.ts +3 -2
  99. package/dist/core/scheme-types.d.ts.map +1 -1
  100. package/dist/core/scheme-types.js.map +1 -1
  101. package/dist/digest/Digest.js +1 -1
  102. package/dist/digest/Digest.js.map +1 -1
  103. package/dist/schemes/Exec.d.ts.map +1 -1
  104. package/dist/schemes/Exec.js +53 -15
  105. package/dist/schemes/Exec.js.map +1 -1
  106. package/dist/schemes/ExecOutputScheme.js +1 -1
  107. package/dist/schemes/ExecOutputScheme.js.map +1 -1
  108. package/dist/schemes/File.d.ts.map +1 -1
  109. package/dist/schemes/File.js +1 -1
  110. package/dist/schemes/File.js.map +1 -1
  111. package/dist/schemes/Log.d.ts.map +1 -1
  112. package/dist/schemes/Log.js +2 -2
  113. package/dist/schemes/Log.sql +2 -2
  114. package/dist/schemes/Worker.js +2 -2
  115. package/dist/schemes/Worker.js.map +1 -1
  116. package/dist/schemes/_entry-crud.js +4 -4
  117. package/dist/schemes/_entry-crud.js.map +1 -1
  118. package/dist/schemes/_entry-crud.sql +2 -2
  119. package/dist/schemes/_entry-find.d.ts +5 -5
  120. package/dist/schemes/_entry-find.d.ts.map +1 -1
  121. package/dist/schemes/_entry-find.js +29 -28
  122. package/dist/schemes/_entry-find.js.map +1 -1
  123. package/dist/schemes/_entry-manifest.js +4 -4
  124. package/dist/schemes/_entry-manifest.js.map +1 -1
  125. package/dist/schemes/_entry-ops.d.ts.map +1 -1
  126. package/dist/schemes/_entry-ops.js +3 -3
  127. package/dist/schemes/_entry-ops.js.map +1 -1
  128. package/dist/schemes/_entry-ops.sql +3 -3
  129. package/dist/server/Daemon.d.ts +20 -49
  130. package/dist/server/Daemon.d.ts.map +1 -1
  131. package/dist/server/Daemon.js +311 -908
  132. package/dist/server/Daemon.js.map +1 -1
  133. package/dist/server/DaemonModule.d.ts +31 -3
  134. package/dist/server/DaemonModule.d.ts.map +1 -1
  135. package/dist/server/DrainSupervisor.d.ts +117 -0
  136. package/dist/server/DrainSupervisor.d.ts.map +1 -0
  137. package/dist/server/DrainSupervisor.js +802 -0
  138. package/dist/server/DrainSupervisor.js.map +1 -0
  139. package/dist/server/client-input.d.ts +2 -0
  140. package/dist/server/client-input.d.ts.map +1 -1
  141. package/dist/server/client-input.js +12 -0
  142. package/dist/server/client-input.js.map +1 -1
  143. package/dist/server/lifecycle-recovery.sql +9 -0
  144. package/dist/server/logEntry.d.ts +1 -1
  145. package/dist/server/logEntry.js +1 -1
  146. package/dist/server/loopDocs.d.ts +1 -0
  147. package/dist/server/loopDocs.d.ts.map +1 -1
  148. package/dist/server/loopDocs.js +37 -8
  149. package/dist/server/loopDocs.js.map +1 -1
  150. package/dist/server/loopDocs.sql +10 -0
  151. package/dist/server/seam-entry-read.sql +2 -2
  152. package/dist/server/workspace-capabilities.sql +19 -0
  153. package/dist/service.js +1 -1
  154. package/dist/service.js.map +1 -1
  155. package/migrations/001_schema.sql +51 -10
  156. package/package.json +34 -33
  157. package/dist/core/ruler_count.d.ts +0 -4
  158. package/dist/core/ruler_count.d.ts.map +0 -1
  159. package/dist/core/ruler_count.js +0 -7
  160. package/dist/core/ruler_count.js.map +0 -1
  161. package/dist/core/token-ruler.d.ts +0 -2
  162. package/dist/core/token-ruler.d.ts.map +0 -1
  163. package/dist/core/token-ruler.js +0 -5
  164. package/dist/core/token-ruler.js.map +0 -1
@@ -9,8 +9,7 @@ import { PathSyntax, TagSignal } from "@plurnk/plurnk-contracts";
9
9
  import Namespace from "./namespace.js";
10
10
  import LoopFlagsReader from "./LoopFlagsReader.js";
11
11
  import ChannelWrite from "./ChannelWrite.js";
12
- import { assertEditBatchReceipt, assertEditReceipt, assertResourceEffects, EditCollision, editReceipt, LineAnchors, LineMarkerOps, MimetypeBinary, PathMimetype, ReadProjector, projectEditReceipt, } from "../content/index.js";
13
- import DbProjectionCaps from "./caps/DbProjectionCaps.js";
12
+ import { ReadProjector } from "../content/index.js";
14
13
  import SchemeCtxImpl from "./caps/SchemeCtxImpl.js";
15
14
  import EntryCrud from "../schemes/_entry-crud.js";
16
15
  import EntryOps from "../schemes/_entry-ops.js";
@@ -24,6 +23,8 @@ import EffectPolicy from "../schemes/EffectPolicy.js";
24
23
  import { CoreSchemeAdapterBase, } from "./CoreSchemeServices.js";
25
24
  import { InvalidOperationResultError, NetworkAddress, } from "@plurnk/plurnk-schemes";
26
25
  import DurableStatement from "./DurableStatement.js";
26
+ import ResourceMutations from "./ResourceMutations.js";
27
+ import LogBody from "./LogBody.js";
27
28
  // SPEC {§scheme-surface}: writer must be in target scheme's manifest.writableBy.
28
29
  // OPEN/FOLD/READ/FIND are not gated — they curate the log or read, never mutating an entry.
29
30
  const MUTATING_OPS = new Set(["EDIT", "SEND", "COPY", "MOVE", "EXEC", "KILL", "FORK", "WORK"]);
@@ -37,9 +38,8 @@ const assertClassifyingSignal = (statement) => {
37
38
  TagSignal.applied(statement.signal);
38
39
  }
39
40
  };
40
- // Op dispatch ({§op-methods-op-dispatch}): gates (writableBy, loop flags), the
41
- // engine-owned op orchestrations (COPY/MOVE/KILL/SEND), scheme
42
- // routing, the durable log write, and the proposal pause.
41
+ // Op dispatch ({§op-methods-op-dispatch}): admission, operation-owner routing,
42
+ // durable log writing, and proposal lifecycle.
43
43
  class Dispatcher {
44
44
  static #failure(code, status, detail, fields = {}, extensions = {}) {
45
45
  return Results.failure("engine:dispatcher", code, status, detail, fields, extensions);
@@ -52,9 +52,10 @@ class Dispatcher {
52
52
  #db;
53
53
  #schemes;
54
54
  #mimetypes;
55
- #tokenize;
55
+ #weighContent;
56
56
  #notices;
57
57
  #proposals;
58
+ #interactions;
58
59
  // Boot-discovered runtime executors, late-injected on Engine — thunked.
59
60
  #executors;
60
61
  // Per-loop abort signal, owned by Engine.runLoop — thunked.
@@ -74,14 +75,15 @@ class Dispatcher {
74
75
  #joinTargets;
75
76
  #liveSubscriptions;
76
77
  #lifecycle;
77
- #preparedEdits = new WeakMap();
78
- constructor({ db, schemes, mimetypes, tokenize, notices, proposals, executors, loopSignal, streamEventNotify, wakeWorkerNotify, injectWorker, branchWorker, branchCompletionGate, cancelWorker, cancelDescendants, searchGate, parkDeadlines, joinTargets, liveSubscriptions }) {
78
+ #resourceMutations;
79
+ constructor({ db, schemes, mimetypes, weigh, notices, proposals, interactions, executors, loopSignal, streamEventNotify, wakeWorkerNotify, injectWorker, branchWorker, branchCompletionGate, cancelWorker, cancelDescendants, searchGate, parkDeadlines, joinTargets, liveSubscriptions }) {
79
80
  this.#db = db;
80
81
  this.#schemes = schemes;
81
82
  this.#mimetypes = mimetypes;
82
- this.#tokenize = tokenize;
83
+ this.#weighContent = weigh;
83
84
  this.#notices = notices;
84
85
  this.#proposals = proposals;
86
+ this.#interactions = interactions;
85
87
  this.#executors = executors;
86
88
  this.#loopSignal = loopSignal;
87
89
  this.#streamEventNotify = streamEventNotify;
@@ -96,26 +98,43 @@ class Dispatcher {
96
98
  this.#joinTargets = joinTargets ?? new Set();
97
99
  this.#liveSubscriptions = liveSubscriptions;
98
100
  this.#lifecycle = new LoopLifecycle(db);
101
+ this.#resourceMutations = new ResourceMutations({
102
+ schemes,
103
+ liveSubscriptions,
104
+ run: (schemeName, statement, ctx) => this.#run(schemeName, statement, ctx),
105
+ checkWritable: (statement, origin, workspaceId) => this.#checkWritable(statement, origin, workspaceId),
106
+ checkFlagsGate: (statement, loopId, workspaceId) => this.#checkFlagsGate(statement, loopId, workspaceId),
107
+ editTargetIdentity: (statement, workspaceId) => this.#editTargetIdentity(statement, workspaceId),
108
+ canonicalFilePath: (pathname, workspaceId) => this.#canonicalFilePath(pathname, workspaceId),
109
+ prepareDataRepresentation: (args) => this.#prepareDataRepresentation({
110
+ ...args,
111
+ handler: args.handler,
112
+ }),
113
+ readEntry: (scheme, pathname, ctx) => this.#readEntry(scheme, pathname, ctx),
114
+ writeEntry: (scheme, pathname, entry, ctx) => this.#writeEntry(scheme, pathname, entry, ctx),
115
+ deleteChannel: (scheme, pathname, channel, ctx) => this.#deleteChannel(scheme, pathname, channel, ctx),
116
+ applyProposal: (statement, result, resolution, ids) => this.#proposals.workerApply(statement, result, resolution, ids),
117
+ });
99
118
  }
100
119
  // workspace → project_root, memoized: {§fs-namespace} fixes the root immutably at
101
120
  // workspace creation, so a process-lifetime cache can never go stale.
102
121
  #rootCache = new Map();
103
122
  #handlerContext(scheme, ctx) {
104
- const manifest = this.#schemes.manifestFor(scheme);
123
+ const manifest = this.#schemes.manifestFor(scheme, ctx.workspaceId);
105
124
  return manifest === undefined ? null : new SchemeCtxImpl(ctx, scheme, manifest, this.#liveSubscriptions);
106
125
  }
107
126
  #entryContext(scheme, ctx) {
108
127
  const handlerCtx = this.#handlerContext(scheme, ctx);
109
- return this.#schemes.manifestFor(scheme)?.category === "data" ? handlerCtx : null;
128
+ return this.#schemes.manifestFor(scheme, ctx.workspaceId)?.category === "data" ? handlerCtx : null;
110
129
  }
111
- #coreCrud(scheme) {
112
- const handler = this.#schemes.get(scheme);
130
+ #coreCrud(scheme, workspaceId) {
131
+ const handler = this.#schemes.get(scheme, workspaceId);
113
132
  return handler instanceof CoreSchemeAdapterBase
114
133
  ? handler
115
134
  : undefined;
116
135
  }
117
136
  async #readEntry(scheme, pathname, ctx) {
118
- const handler = this.#coreCrud(scheme);
137
+ const handler = this.#coreCrud(scheme, ctx.workspaceId);
119
138
  const handlerCtx = this.#entryContext(scheme, ctx);
120
139
  if (typeof handler?.readEntry === "function" && handlerCtx !== null) {
121
140
  return Results.assert(await handler.readEntry(pathname, handlerCtx));
@@ -144,7 +163,7 @@ class Dispatcher {
144
163
  });
145
164
  }
146
165
  async #writeEntry(scheme, pathname, entry, ctx) {
147
- const handler = this.#coreCrud(scheme);
166
+ const handler = this.#coreCrud(scheme, ctx.workspaceId);
148
167
  const handlerCtx = this.#entryContext(scheme, ctx);
149
168
  if (typeof handler?.writeEntry === "function" && handlerCtx !== null) {
150
169
  return Results.assert(await handler.writeEntry(pathname, entry, handlerCtx));
@@ -161,7 +180,7 @@ class Dispatcher {
161
180
  return Results.assert(await caps.write(pathname, entry, scheme === "prompt" ? "worker" : "commons"));
162
181
  }
163
182
  async #deleteEntry(scheme, pathname, ctx) {
164
- const handler = this.#coreCrud(scheme);
183
+ const handler = this.#coreCrud(scheme, ctx.workspaceId);
165
184
  const handlerCtx = this.#entryContext(scheme, ctx);
166
185
  if (typeof handler?.deleteEntry === "function" && handlerCtx !== null) {
167
186
  return Results.assert(await handler.deleteEntry(pathname, handlerCtx));
@@ -178,7 +197,7 @@ class Dispatcher {
178
197
  return Results.assert(await caps.delete(pathname, scheme === "prompt" ? "worker" : "commons"));
179
198
  }
180
199
  async #deleteChannel(scheme, pathname, channel, ctx) {
181
- const handler = this.#coreCrud(scheme);
200
+ const handler = this.#coreCrud(scheme, ctx.workspaceId);
182
201
  const handlerCtx = this.#entryContext(scheme, ctx);
183
202
  if (typeof handler?.deleteChannel === "function" && handlerCtx !== null) {
184
203
  return Results.assert(await handler.deleteChannel(pathname, channel, handlerCtx));
@@ -213,230 +232,35 @@ class Dispatcher {
213
232
  if (key !== null)
214
233
  target.pathname = key;
215
234
  }
216
- async #resolveEditAnchors(statements, identity, schemeName, manifest, ctx) {
217
- const anchored = statements.filter(({ lineMarker }) => LineAnchors.hasAnchor(lineMarker));
218
- if (anchored.length === 0) {
219
- return { statements: statements, precondition: null };
220
- }
221
- if (manifest.textEditScopes !== true || !manifest.writableBy.includes("model")) {
222
- return {
223
- result: _a.#failure("line-anchor-unsupported", 400, `Scheme '${schemeName}' does not support textual EDIT scopes.`, {}, {
224
- scheme: schemeName,
225
- operation: "EDIT",
226
- recovery: "Remove the scope and submit the scheme's complete editable unit.",
227
- retryable: false,
228
- }),
229
- };
230
- }
231
- if (identity === null) {
232
- return {
233
- result: _a.#failure("edit-target-required", 400, "A line-anchored EDIT requires a target resource.", {}, { recovery: "Provide the target that rendered the line anchor.", retryable: false }),
234
- };
235
- }
236
- const first = statements[0];
237
- if (first === undefined)
238
- return { statements: [], precondition: null };
239
- const current = await this.#run(schemeName, {
240
- op: "READ",
241
- suffix: first.suffix,
242
- signal: null,
243
- target: first.target,
244
- lineMarker: { marks: [1, -1] },
245
- body: null,
246
- position: first.position,
247
- }, ctx);
248
- if (current.status === 204 || current.status === 404) {
249
- return { result: EditCollision.result(identity) };
250
- }
251
- if (current.status >= 300) {
252
- return {
253
- result: _a.#failure("line-anchor-validation-failed", current.status, `EDIT could not validate its line anchor at ${identity}: ${current.problem?.detail ?? `READ returned ${current.status}`}`, {}, {
254
- target: identity,
255
- upstreamStatus: current.status,
256
- stage: "mutation-precondition",
257
- retryable: false,
258
- }),
259
- };
260
- }
261
- if (current.status !== 200) {
262
- return { result: EditCollision.result(identity) };
263
- }
264
- const content = current.content;
265
- if (typeof content !== "string") {
266
- throw new InvalidOperationResultError(`Scheme '${schemeName}' returned READ ${current.status} without textual content while validating an EDIT anchor.`);
267
- }
268
- const lineAnchors = current.lineAnchors;
269
- const lineAnchorIdentity = current.lineAnchorIdentity;
270
- try {
271
- LineAnchors.assertProjection(content, lineAnchors);
272
- if (typeof lineAnchorIdentity !== "string" || lineAnchorIdentity.length === 0) {
273
- throw new TypeError("READ line anchors require their canonical derivation identity.");
274
- }
275
- }
276
- catch (cause) {
277
- throw new InvalidOperationResultError(`Scheme '${schemeName}' returned READ 200 without its core-owned line-anchor projection.`, { cause });
278
- }
279
- const resolved = [];
280
- const checks = [];
281
- for (const statement of statements) {
282
- if (statement.lineMarker === null || !LineAnchors.hasAnchor(statement.lineMarker)) {
283
- resolved.push(statement);
284
- continue;
285
- }
286
- const resolution = LineAnchors.resolve(lineAnchors, statement.lineMarker);
287
- if (!resolution.ok) {
288
- const { anchor, kind } = resolution.failure;
289
- const invalid = kind === "invalid";
290
- if (!invalid)
291
- return { result: EditCollision.result(lineAnchorIdentity) };
292
- return {
293
- result: _a.#failure("line-anchor-invalid", 400, `${anchor} is not valid in that EDIT line-coordinate position.`, {}, {
294
- anchor,
295
- target: identity,
296
- recovery: "Use anchors only where the EDIT scope denotes a line.",
297
- retryable: false,
298
- }),
299
- };
300
- }
301
- for (const [index, anchor] of statement.lineMarker.marks.entries()) {
302
- if (typeof anchor !== "string")
303
- continue;
304
- const line = resolution.marker.marks[index];
305
- if (typeof line !== "number") {
306
- throw new InvalidOperationResultError("An EDIT line anchor did not lower to a numeric line.");
307
- }
308
- checks.push({ anchor, line });
309
- }
310
- resolved.push({ ...statement, lineMarker: resolution.marker });
311
- }
312
- const uniqueChecks = [...new Map(checks.map((check) => [`${check.anchor}:${check.line}`, check])).values()];
313
- return {
314
- statements: resolved,
315
- precondition: { identity: lineAnchorIdentity, checks: uniqueChecks },
316
- };
235
+ async #canonicalFilePath(pathname, workspaceId) {
236
+ return Namespace.canonicalizeSpelling(pathname, await this.#workspaceRoot(workspaceId));
317
237
  }
318
- async prepareEditBatches(statements, context) {
319
- const { workspaceId, workerId, loopId, turnId, origin } = context;
320
- const schemeCtx = this.#buildSchemeCtx({ workspaceId, workerId, loopId, turnId, origin });
321
- const groups = new Map();
322
- for (const statement of statements) {
323
- assertClassifyingSignal(statement); // {§log-tag-signal}
324
- const target = this.#extractTarget(statement.target);
325
- await this.#canonColumns(target, workspaceId);
326
- const key = JSON.stringify([
238
+ async #editTargetIdentity(statement, workspaceId) {
239
+ const target = this.#extractTarget(statement.target, workspaceId);
240
+ await this.#canonColumns(target, workspaceId);
241
+ return {
242
+ key: JSON.stringify([
327
243
  schemeNameOf(statement.target),
328
244
  target.scheme,
329
245
  target.hostname,
330
246
  target.pathname,
331
247
  target.fragment,
332
- ]);
333
- const group = groups.get(key);
334
- if (group === undefined)
335
- groups.set(key, {
336
- identity: renderTarget(target),
337
- statements: [statement],
338
- });
339
- else
340
- group.statements.push(statement);
341
- }
342
- for (const preparedGroup of groups.values()) {
343
- const group = preparedGroup.statements;
344
- const first = group[0];
345
- const schemeName = schemeNameOf(first.target);
346
- let initial;
347
- let denial = group.map((statement) => this.#checkWritable(statement, origin)).find((result) => result !== null) ?? null;
348
- if (denial === null) {
349
- for (const statement of group) {
350
- denial = await this.#checkFlagsGate(statement, loopId);
351
- if (denial !== null)
352
- break;
353
- }
354
- }
355
- if (denial !== null) {
356
- initial = denial;
357
- }
358
- else if (schemeName === null) {
359
- initial = _a.#failure("target-required", 400, "EDIT requires a target scheme.", {}, { retryable: false });
360
- }
361
- else {
362
- const handler = this.#schemes.get(schemeName);
363
- const method = handler?.editBatch;
364
- const manifest = this.#schemes.manifestFor(schemeName);
365
- if (typeof method !== "function" || manifest === undefined) {
366
- initial = _a.#failure("operation-not-implemented", 501, `Scheme '${schemeName}' does not implement EDIT batches.`, {}, {
367
- scheme: schemeName,
368
- operation: "EDIT",
369
- retryable: false,
370
- });
371
- }
372
- else {
373
- try {
374
- const resolved = await this.#resolveEditAnchors(group, preparedGroup.identity, schemeName, manifest, schemeCtx);
375
- if ("result" in resolved) {
376
- initial = resolved.result;
377
- }
378
- else {
379
- const addressedScheme = first.target?.kind === "url" ? first.target.scheme : schemeName;
380
- const publishedChannel = first.target?.kind === "url"
381
- ? first.target.fragment ?? manifest.defaultChannel
382
- : manifest.defaultChannel;
383
- initial = Results.assert(await method.call(handler, resolved.statements, new SchemeCtxImpl(schemeCtx, addressedScheme ?? schemeName, manifest, this.#liveSubscriptions, {
384
- publishedChannel,
385
- editPrecondition: resolved.precondition,
386
- })));
387
- }
388
- }
389
- catch (err) {
390
- if (err instanceof InvalidOperationResultError)
391
- throw err;
392
- console.error(`Scheme '${schemeName}' EDIT batch threw outside its operation result contract:`, err);
393
- initial = _a.#failure("scheme-handler-threw", 500, `The '${schemeName}' scheme did not produce an EDIT result.`, {}, {
394
- stage: "scheme-dispatch",
395
- scheme: schemeName,
396
- operation: "EDIT",
397
- });
398
- }
399
- }
400
- }
401
- let resolveSettled;
402
- const settled = new Promise((resolve) => { resolveSettled = resolve; });
403
- const candidate = initial.editReceipt;
404
- const expectedNormalizations = group.filter(({ lineMarker }) => lineMarker?.marks.length === 3).length;
405
- if (initial.status < 400
406
- && (initial.scopeNormalizations?.length ?? 0) !== expectedNormalizations) {
407
- throw new InvalidOperationResultError(`EDIT batch normalized ${initial.scopeNormalizations?.length ?? 0} scope(s), expected ${expectedNormalizations}.`);
408
- }
409
- const batch = {
410
- initial,
411
- settled,
412
- aggregate: candidate === undefined || candidate === null
413
- ? undefined
414
- : assertEditBatchReceipt(candidate),
415
- settle: resolveSettled,
416
- };
417
- let normalizationIndex = 0;
418
- for (const [index, statement] of group.entries()) {
419
- const ownsNormalization = statement.lineMarker?.marks.length === 3 && initial.status < 400;
420
- this.#preparedEdits.set(statement, {
421
- first: index === 0,
422
- index,
423
- normalizationIndex: ownsNormalization ? normalizationIndex++ : null,
424
- batch,
425
- });
426
- }
427
- }
248
+ ]),
249
+ identity: renderTarget(target),
250
+ };
251
+ }
252
+ async prepareEditBatches(statements, context) {
253
+ for (const statement of statements)
254
+ assertClassifyingSignal(statement); // {§log-tag-signal}
255
+ const { workspaceId, workerId, loopId, turnId, origin } = context;
256
+ const schemeCtx = this.#buildSchemeCtx({ workspaceId, workerId, loopId, turnId, origin });
257
+ await this.#resourceMutations.prepareEditBatches(statements, context, schemeCtx);
428
258
  }
429
259
  async dispatch(context) {
430
260
  assertClassifyingSignal(context.statement); // {§log-tag-signal}
431
261
  const result = await this.#dispatchOne(context);
432
262
  if (context.statement.op === "EDIT") {
433
- const prepared = this.#preparedEdits.get(context.statement);
434
- if (prepared?.first === true) {
435
- const normalizations = prepared.batch.initial.scopeNormalizations;
436
- prepared.batch.settle(normalizations === undefined
437
- ? result
438
- : Results.assert({ ...result, scopeNormalizations: normalizations }));
439
- }
263
+ this.#resourceMutations.settleEdit(context.statement, result);
440
264
  }
441
265
  return result;
442
266
  }
@@ -445,9 +269,9 @@ class Dispatcher {
445
269
  const schemeCtx = this.#buildSchemeCtx({ workspaceId, workerId, loopId, turnId, origin });
446
270
  let result;
447
271
  let curationPlan = null;
448
- let denial = this.#checkWritable(statement, origin);
272
+ let denial = this.#checkWritable(statement, origin, workspaceId);
449
273
  if (denial === null)
450
- denial = await this.#checkFlagsGate(statement, loopId);
274
+ denial = await this.#checkFlagsGate(statement, loopId, workspaceId);
451
275
  if (denial !== null) {
452
276
  result = denial;
453
277
  }
@@ -459,34 +283,7 @@ class Dispatcher {
459
283
  // those are system failures.
460
284
  try {
461
285
  if (statement.op === "EDIT") {
462
- const prepared = this.#preparedEdits.get(statement);
463
- if (prepared === undefined) {
464
- throw new InvalidOperationResultError("EDIT reached dispatch without a prepared resource batch.");
465
- }
466
- else {
467
- const settled = prepared.first
468
- ? prepared.batch.initial
469
- : await prepared.batch.settled;
470
- const normalization = prepared.normalizationIndex === null
471
- ? undefined
472
- : settled.scopeNormalizations?.[prepared.normalizationIndex];
473
- const { scopeNormalizations: _scopeNormalizations, ...projectedSettlement } = settled;
474
- const statementResult = normalization === undefined
475
- ? projectedSettlement
476
- : { ...projectedSettlement, scopeNormalizations: [normalization] };
477
- const aggregate = prepared.batch.aggregate;
478
- if (aggregate !== undefined) {
479
- const receipt = assertEditBatchReceipt(aggregate);
480
- const { editReceipt: _editReceipt, ...withoutAggregate } = statementResult;
481
- result = {
482
- ...withoutAggregate,
483
- receipt: projectEditReceipt(receipt, prepared.index),
484
- };
485
- }
486
- else {
487
- result = statementResult;
488
- }
489
- }
286
+ result = await this.#resourceMutations.preparedEditResult(statement);
490
287
  }
491
288
  else if (statement.op === "SEND" && statement.target === null) {
492
289
  result = await this.#handleSendBroadcast(statement, { workspaceId, workerId, loopId, turnId, sequence });
@@ -500,10 +297,10 @@ class Dispatcher {
500
297
  result = await this.#handleWorkerControl(statement, schemeCtx);
501
298
  }
502
299
  else if (statement.op === "COPY") {
503
- result = await this.#handleCopy(statement, schemeCtx);
300
+ result = await this.#resourceMutations.handleCopy(statement, schemeCtx);
504
301
  }
505
302
  else if (statement.op === "MOVE") {
506
- result = await this.#handleMove(statement, schemeCtx);
303
+ result = await this.#resourceMutations.handleMove(statement, schemeCtx);
507
304
  }
508
305
  else if (statement.op === "KILL") {
509
306
  result = await this.#handleKill(statement, schemeCtx);
@@ -583,15 +380,13 @@ class Dispatcher {
583
380
  }
584
381
  if (autoAccept) {
585
382
  const initialSettlement = await this.#proposals.workerApply(statement, result, { decision: "accept" }, { workspaceId, workerId, loopId, turnId });
586
- const settlement = _a.#settleProposalEffects(result, initialSettlement);
587
- const effective = await this.#settleMoveProposal({
383
+ const effective = await this.#resourceMutations.settleProposal({
588
384
  statement,
589
385
  result,
590
- settlement,
386
+ settlement: initialSettlement,
591
387
  ctx: schemeCtx,
592
388
  ids: { workspaceId, workerId, loopId, turnId },
593
389
  });
594
- this.#recordEditSettlement(statement, effective);
595
390
  return this.#proposals.applyResolution(logEntryId, effective);
596
391
  }
597
392
  // Register the resolution waiter SYNCHRONOUSLY before any await
@@ -620,15 +415,13 @@ class Dispatcher {
620
415
  // preserved: an apply failure keeps its original status and
621
416
  // Problem Details instead of masquerading as a client rejection.
622
417
  const initialSettlement = await this.#proposals.workerApply(statement, result, resolution, { workspaceId, workerId, loopId, turnId });
623
- const settlement = _a.#settleProposalEffects(result, initialSettlement);
624
- const effective = await this.#settleMoveProposal({
418
+ const effective = await this.#resourceMutations.settleProposal({
625
419
  statement,
626
420
  result,
627
- settlement,
421
+ settlement: initialSettlement,
628
422
  ctx: schemeCtx,
629
423
  ids: { workspaceId, workerId, loopId, turnId },
630
424
  });
631
- this.#recordEditSettlement(statement, effective);
632
425
  const post = await this.#proposals.applyResolution(logEntryId, effective);
633
426
  return post;
634
427
  }
@@ -645,7 +438,7 @@ class Dispatcher {
645
438
  throw new Error(`look resolves READ only; got ${statement.op}`);
646
439
  // turnId is a write-time FK only — a look writes no row, so 0 (no turn) is inert.
647
440
  const schemeCtx = this.#buildSchemeCtx({ workspaceId, workerId, loopId, turnId: 0, origin });
648
- const denial = await this.#checkFlagsGate(statement, loopId);
441
+ const denial = await this.#checkFlagsGate(statement, loopId, workspaceId);
649
442
  if (denial !== null)
650
443
  return denial;
651
444
  return this.#run(schemeNameOf(statement.target), statement, schemeCtx);
@@ -658,8 +451,8 @@ class Dispatcher {
658
451
  const routedScheme = schemeNameOf(target);
659
452
  if (routedScheme === null)
660
453
  return null;
661
- const handler = this.#schemes.get(routedScheme);
662
- const manifest = this.#schemes.manifestFor(routedScheme);
454
+ const handler = this.#schemes.get(routedScheme, workspaceId);
455
+ const manifest = this.#schemes.manifestFor(routedScheme, workspaceId);
663
456
  if (handler === undefined || manifest?.category !== "data")
664
457
  return null;
665
458
  const addressedScheme = target.kind === "url" ? target.scheme : routedScheme;
@@ -784,7 +577,7 @@ class Dispatcher {
784
577
  // handler and addressed context as an authored READ. {§exec-target-routing}
785
578
  async readExecSource(statement, ctx) {
786
579
  const schemeName = schemeNameOf(statement.target);
787
- const manifest = schemeName === null ? undefined : this.#schemes.manifestFor(schemeName);
580
+ const manifest = schemeName === null ? undefined : this.#schemes.manifestFor(schemeName, ctx.workspaceId);
788
581
  if (manifest !== undefined && manifest.category !== "data") {
789
582
  return _a.#failure("exec-source-not-data", 501, `Scheme '${schemeName}' is not a data source for EXEC.`, {}, {
790
583
  scheme: schemeName,
@@ -805,8 +598,9 @@ class Dispatcher {
805
598
  wakeWorkerNotify: this.#wakeWorkerNotify,
806
599
  injectWorker: this.#injectWorker,
807
600
  mimetypes: this.#mimetypes,
808
- tokenize: this.#tokenize,
601
+ weigh: this.#weighContent,
809
602
  pushNotice: (notice) => this.#notices.push(workspaceId, loopId, notice),
603
+ requestInteraction: (request) => this.#interactions.request(request, { workspaceId, workerId, loopId, turnId }, this.#loopSignal(loopId)),
810
604
  executors: this.#executors(),
811
605
  };
812
606
  }
@@ -816,7 +610,7 @@ class Dispatcher {
816
610
  // - SEND broadcast (path=null) has no target scheme; not gated.
817
611
  // - COPY: dst scheme writableBy applies.
818
612
  // - MOVE: both src (delete) and dst (write) schemes' writableBy apply.
819
- #checkWritable(statement, origin) {
613
+ #checkWritable(statement, origin, workspaceId) {
820
614
  if (!MUTATING_OPS.has(statement.op))
821
615
  return null;
822
616
  if (statement.op === "SEND" && statement.target === null)
@@ -824,23 +618,23 @@ class Dispatcher {
824
618
  // EXEC's operation authority always belongs to the exec scheme;
825
619
  // runtime-specific resource authority is gated separately below.
826
620
  if (statement.op === "EXEC") {
827
- return this.#denyIfDisallowed("exec", origin);
621
+ return this.#denyIfDisallowed("exec", origin, workspaceId);
828
622
  }
829
623
  // Worker control (FORK/WORK → worker://<name>, spawn or fork) is gated by worker://'s writableBy — its
830
624
  // body is a seed prompt, not a dst path, so the entry-COPY dst-parse below doesn't apply.
831
625
  // {§machine-processes}
832
626
  if (this.#isWorkerControl(statement))
833
- return this.#denyIfDisallowed("worker", origin);
627
+ return this.#denyIfDisallowed("worker", origin, workspaceId);
834
628
  if (statement.op === "COPY" || statement.op === "MOVE") {
835
629
  const dst = statement.body?.target ?? null;
836
630
  const dstScheme = schemeNameOf(dst);
837
- const dstDenial = this.#denyIfDisallowed(dstScheme, origin);
631
+ const dstDenial = this.#denyIfDisallowed(dstScheme, origin, workspaceId);
838
632
  if (dstDenial !== null)
839
633
  return dstDenial;
840
634
  if (statement.op === "MOVE") {
841
635
  const srcScheme = schemeNameOf(statement.target);
842
636
  if (srcScheme !== dstScheme) {
843
- const srcDenial = this.#denyIfDisallowed(srcScheme, origin);
637
+ const srcDenial = this.#denyIfDisallowed(srcScheme, origin, workspaceId);
844
638
  if (srcDenial !== null)
845
639
  return srcDenial;
846
640
  }
@@ -848,7 +642,7 @@ class Dispatcher {
848
642
  return null;
849
643
  }
850
644
  const target = schemeNameOf(statement.target);
851
- return this.#denyIfDisallowed(target, origin);
645
+ return this.#denyIfDisallowed(target, origin, workspaceId);
852
646
  }
853
647
  // {§search-gate} — gate only configured search runtimes; duplicates serve
854
648
  // the prior durable digest and the per-turn cap refuses without execution.
@@ -880,13 +674,13 @@ class Dispatcher {
880
674
  }
881
675
  return this.#run("exec", statement, ctx);
882
676
  }
883
- #denyIfDisallowed(schemeName, origin) {
677
+ #denyIfDisallowed(schemeName, origin, workspaceId) {
884
678
  if (schemeName === null)
885
679
  return null;
886
- const handler = this.#schemes.get(schemeName);
680
+ const handler = this.#schemes.get(schemeName, workspaceId);
887
681
  if (handler === undefined)
888
682
  return null;
889
- const manifest = this.#schemes.manifestFor(schemeName);
683
+ const manifest = this.#schemes.manifestFor(schemeName, workspaceId);
890
684
  if (manifest === undefined)
891
685
  throw new Error(`registered scheme '${schemeName}' has no manifest`);
892
686
  if (manifest.writableBy.includes(origin))
@@ -904,7 +698,7 @@ class Dispatcher {
904
698
  // active set under the loop's persisted flags. A registered scheme outside
905
699
  // that set returns 403; unknown names continue to their operation owner for
906
700
  // the ordinary registration failure. Action-entry-as-outcome carries either.
907
- async #checkFlagsGate(statement, loopId) {
701
+ async #checkFlagsGate(statement, loopId, workspaceId) {
908
702
  // Broadcast SEND has no scheme to gate.
909
703
  if (statement.op === "SEND" && statement.target === null)
910
704
  return null;
@@ -938,7 +732,7 @@ class Dispatcher {
938
732
  });
939
733
  }
940
734
  }
941
- const active = this.#schemes.resolveForLoop(flags);
735
+ const active = this.#schemes.resolveForLoop(flags, workspaceId);
942
736
  // {§tools-loop-affinity}: name the non-retryable restriction so the model changes course.
943
737
  const restriction = flags.mode === "ask"
944
738
  ? "this is an ask-mode (read-only) loop — you cannot run commands or take host actions here"
@@ -946,7 +740,7 @@ class Dispatcher {
946
740
  : flags.noWeb ? "web access is disabled for this loop"
947
741
  : "interaction is disabled for this loop";
948
742
  const checkScheme = (scheme) => {
949
- if (scheme === null || !this.#schemes.has(scheme))
743
+ if (scheme === null || !this.#schemes.has(scheme, workspaceId))
950
744
  return null;
951
745
  if (active.has(scheme))
952
746
  return null;
@@ -970,7 +764,7 @@ class Dispatcher {
970
764
  return operationDenial;
971
765
  const requested = typeof statement.signal === "string" ? statement.signal : "";
972
766
  const runtime = requested === "" ? "sh" : requested;
973
- const targetKind = this.#executors()?.entry(runtime)?.invocation.target?.kind;
767
+ const targetKind = this.#executors()?.entry(runtime, workspaceId)?.invocation.target?.kind;
974
768
  if (targetKind !== "resource")
975
769
  return null;
976
770
  const sourceScheme = schemeNameOf(statement.target);
@@ -979,7 +773,7 @@ class Dispatcher {
979
773
  return check(statement.target);
980
774
  }
981
775
  // Worker control is FORK/WORK (grammar 0.74.55), not COPY — its body
982
- // is the new worker's seed prompt, not a destination path. The COPY gates and #handleCopy
776
+ // is the new worker's seed prompt, not a destination path. The COPY gates and ResourceMutations.handleCopy
983
777
  // branch on this so they never parse the prompt as a dst path.
984
778
  #isWorkerControl(statement) {
985
779
  return statement.op === "FORK" || statement.op === "WORK"; // worker control targets worker://<name> (grammar 0.74.55)
@@ -1056,53 +850,6 @@ class Dispatcher {
1056
850
  await ctx.injectWorker({ workspaceId: ctx.workspaceId, workerId: row.id, prompt, flags, parentLoopId: ctx.loopId });
1057
851
  return { status: 200, body: name };
1058
852
  }
1059
- async #handleCopy(statement, ctx) {
1060
- if (statement.target === null) {
1061
- return _a.#failure("copy-source-required", 400, "COPY requires a source path.", {}, { retryable: false });
1062
- }
1063
- if (statement.body === null) {
1064
- return _a.#failure("copy-destination-required", 400, "COPY requires a destination.", {}, { retryable: false });
1065
- }
1066
- return this.#copyOrchestration({
1067
- statement,
1068
- source: {
1069
- target: statement.target,
1070
- lineMarker: statement.lineMarker,
1071
- },
1072
- destination: statement.body,
1073
- ctx,
1074
- });
1075
- }
1076
- async #handleMove(statement, ctx) {
1077
- if (statement.target === null) {
1078
- return _a.#failure("move-source-required", 400, "MOVE requires a source path.", {}, { retryable: false });
1079
- }
1080
- // MOVE is relocation only - deletion is KILL's job ({§move}, {§move-dev-null-not-special}). The /dev/null
1081
- // and null-body delete-by-MOVE has no alternate meaning.
1082
- if (statement.body === null) {
1083
- return _a.#failure("move-destination-required", 400, "MOVE requires a destination.", {}, {
1084
- recovery: "Use KILL when the intended operation is deletion.",
1085
- retryable: false,
1086
- });
1087
- }
1088
- const sourceMarks = statement.lineMarker?.marks;
1089
- const sourceLineMarker = sourceMarks?.length === 2
1090
- && sourceMarks[0] === 1
1091
- && sourceMarks[1] === -1
1092
- ? null
1093
- : statement.lineMarker;
1094
- return this.#moveOrchestration({
1095
- statement,
1096
- source: {
1097
- target: statement.target,
1098
- // Canonicalize only the execution selection. #writeLog retains
1099
- // the authored marker as operation evidence. {§move-canonical-whole-source}
1100
- lineMarker: sourceLineMarker,
1101
- },
1102
- destination: statement.body,
1103
- ctx,
1104
- });
1105
- }
1106
853
  // KILL is target-polymorphic. Scheme handlers own the optional numeric code's
1107
854
  // meaning; core retains worker and entry dispatch. {§operation-code-polymorphism}
1108
855
  async #handleKill(statement, ctx) {
@@ -1121,7 +868,7 @@ class Dispatcher {
1121
868
  // Process-KILL: any scheme whose handler exposes kill() aborts a live stream — the
1122
869
  // exec handler, registered as "exec" + under every runtime tag (sh/node), so a tag-
1123
870
  // addressed stream (sh:///l/t/s) routes here, not to deleteEntry. {§exec}
1124
- const killable = this.#schemes.get(schemeName);
871
+ const killable = this.#schemes.get(schemeName, ctx.workspaceId);
1125
872
  if (killable !== undefined && typeof killable.kill === "function") {
1126
873
  // Pass the model's OWN scheme so a stream-KILL error answers in the runtime tag the
1127
874
  // model addressed (sh:///…), not the internal `exec` ({§fs-answer-in-canon}).
@@ -1165,7 +912,7 @@ class Dispatcher {
1165
912
  await this.#cancelWorker(workerId, "killed via worker:// KILL");
1166
913
  return { status: 200 };
1167
914
  }
1168
- if (!this.#schemes.has(schemeName)) {
915
+ if (!this.#schemes.has(schemeName, ctx.workspaceId)) {
1169
916
  return _a.#failure("scheme-not-found", 501, `Scheme '${schemeName}' is not registered.`, {}, { scheme: schemeName, retryable: false });
1170
917
  }
1171
918
  // A host-effecting delete (file) returns 202 to PROPOSE — pass its attrs through so the proposal
@@ -1173,6 +920,8 @@ class Dispatcher {
1173
920
  return this.#deleteEntry(schemeName, entryPathnameOf(path), ctx);
1174
921
  }
1175
922
  async #writeActionlessEntry({ verbatim, workerId, loopId, turnId, sequence, origin, kind, folded, modelCallId = null, attrs = {} }) {
923
+ const durableAttrs = { ...attrs, kind };
924
+ const rx = JSON.stringify({ content: verbatim, mimetype: "text/vnd.plurnk" });
1176
925
  const row = await this.#db.engine_insert_log_entry.get({
1177
926
  worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence,
1178
927
  origin, source: null, model_call_id: modelCallId,
@@ -1180,10 +929,19 @@ class Dispatcher {
1180
929
  scheme: null, username: null, password: null, hostname: null, port: null,
1181
930
  pathname: null, query: null, fragment: null, lineMarker: null,
1182
931
  tx: "", mimetype_tx: "text/vnd.plurnk",
1183
- rx: JSON.stringify({ content: verbatim, mimetype: "text/vnd.plurnk" }),
932
+ rx,
1184
933
  mimetype_rx: "application/json",
1185
- status_rx: 200, tokens: this.#tokenize(verbatim), state: "resolved", outcome: null,
1186
- attrs: JSON.stringify({ ...attrs, kind }),
934
+ status_rx: 200,
935
+ weight: LogBody.weight({
936
+ op: null,
937
+ attrs: durableAttrs,
938
+ tx: "",
939
+ rx,
940
+ mimetypeTx: "text/vnd.plurnk",
941
+ mimetypeRx: "application/json",
942
+ }, this.#weighContent),
943
+ state: "resolved", outcome: null,
944
+ attrs: JSON.stringify(durableAttrs),
1187
945
  });
1188
946
  if (row === undefined)
1189
947
  throw new Error("Dispatcher.#writeActionlessEntry: insert returned no row");
@@ -1236,804 +994,6 @@ class Dispatcher {
1236
994
  context.onDispatch?.(logEntryId);
1237
995
  return result;
1238
996
  }
1239
- static #isDispatchResult(value) {
1240
- return "status" in value;
1241
- }
1242
- async #resolveResourceSelection(selection, ctx) {
1243
- const { target, lineMarker } = selection;
1244
- const scheme = schemeNameOf(target);
1245
- if (scheme === null) {
1246
- return _a.#failure("resource-scheme-required", 400, "COPY and MOVE resources require a scheme.", {}, { retryable: false });
1247
- }
1248
- if (target.kind === "url" && target.scheme === "worker" && (target.hostname ?? "") !== "") {
1249
- return _a.#failure("worker-copy-address-invalid", 400, "COPY and MOVE do not address named worker spaces.", {}, {
1250
- recovery: "Move worker-space content with READ and EDIT.",
1251
- retryable: false,
1252
- });
1253
- }
1254
- const handler = this.#schemes.get(scheme);
1255
- const manifest = this.#schemes.manifestFor(scheme);
1256
- if (handler === undefined || manifest === undefined) {
1257
- return _a.#failure("scheme-not-found", 501, `COPY or MOVE addressed the unregistered scheme '${scheme}'.`, {}, {
1258
- scheme,
1259
- retryable: false,
1260
- });
1261
- }
1262
- if (manifest.category !== "data") {
1263
- return _a.#failure("entry-operation-unsupported", 400, `COPY and MOVE require entry-bearing resources; '${scheme}' is a ${manifest.category} scheme.`, {}, {
1264
- scheme,
1265
- category: manifest.category,
1266
- retryable: false,
1267
- });
1268
- }
1269
- const fragment = target.kind === "url" ? target.fragment : null;
1270
- const channel = fragment ?? manifest.defaultChannel;
1271
- if (channel.length === 0) {
1272
- return _a.#failure("channel-required", 400, `The '${scheme}' scheme has no default channel.`, {}, {
1273
- scheme,
1274
- recovery: "Address a named channel with a URI fragment.",
1275
- retryable: false,
1276
- });
1277
- }
1278
- if (fragment !== null
1279
- && fragment !== manifest.defaultChannel
1280
- && !Object.hasOwn(manifest.channels, fragment)) {
1281
- const availableChannels = [
1282
- ...new Set([manifest.defaultChannel, ...Object.keys(manifest.channels)]),
1283
- ].filter((candidate) => candidate.length > 0);
1284
- return _a.#failure("channel-not-found", 400, `Channel #${fragment} is not declared by the '${scheme}' scheme.`, {}, {
1285
- requestedChannel: fragment,
1286
- availableChannels,
1287
- retryable: false,
1288
- });
1289
- }
1290
- const pathname = entryPathnameOf(target);
1291
- const canonicalFilePath = scheme === "file"
1292
- ? Namespace.canonicalizeSpelling(pathname, await this.#workspaceRoot(ctx.workspaceId))
1293
- : pathname;
1294
- return {
1295
- target,
1296
- lineMarker,
1297
- scheme,
1298
- pathname,
1299
- identityPathname: canonicalFilePath ?? pathname,
1300
- channel,
1301
- manifest,
1302
- };
1303
- }
1304
- #resolveResourceLineMarker(selection, content, operation) {
1305
- if (!LineAnchors.hasAnchor(selection.lineMarker)) {
1306
- return {
1307
- selection: {
1308
- ...selection,
1309
- lineMarker: selection.lineMarker,
1310
- },
1311
- precondition: null,
1312
- };
1313
- }
1314
- const target = this.#resourceAddress(selection);
1315
- if (selection.manifest.textEditScopes !== true || !selection.manifest.writableBy.includes("model")) {
1316
- return {
1317
- result: _a.#failure("line-anchor-unsupported", 400, `The representation at ${target} does not publish line anchors.`, {}, {
1318
- operation,
1319
- target,
1320
- recovery: "Use numeric text coordinates.",
1321
- retryable: false,
1322
- }),
1323
- };
1324
- }
1325
- const resolution = LineAnchors.resolve(LineAnchors.tokens(target, content), selection.lineMarker);
1326
- if (!resolution.ok) {
1327
- if (resolution.failure.kind === "invalid") {
1328
- return {
1329
- result: _a.#failure("line-anchor-invalid", 400, "A line anchor appeared outside a line-coordinate position.", {}, {
1330
- operation,
1331
- target,
1332
- recovery: "Use anchors only for L, SL, or EL; columns remain numeric.",
1333
- retryable: false,
1334
- }),
1335
- };
1336
- }
1337
- return {
1338
- result: _a.#failure("line-anchor-collision", 409, `${operation} coordinates collided with current content at ${target}.`, {}, {
1339
- operation,
1340
- target,
1341
- recovery: `READ ${target} again and retry against its current coordinates.`,
1342
- retryable: true,
1343
- }),
1344
- };
1345
- }
1346
- return {
1347
- selection: { ...selection, lineMarker: resolution.marker },
1348
- precondition: {
1349
- identity: target,
1350
- checks: LineAnchors.checks(selection.lineMarker, resolution.marker),
1351
- },
1352
- };
1353
- }
1354
- async #selectSource(selection, ctx, operation) {
1355
- const handler = this.#schemes.get(selection.scheme);
1356
- if (handler === undefined) {
1357
- throw new InvalidOperationResultError(`Resolved COPY/MOVE source scheme '${selection.scheme}' is no longer registered.`);
1358
- }
1359
- const addressedScheme = selection.target.kind === "url"
1360
- ? selection.target.scheme
1361
- : selection.scheme;
1362
- const schemeCtx = new SchemeCtxImpl(ctx, addressedScheme, selection.manifest, this.#liveSubscriptions, { publishedChannel: selection.channel });
1363
- const prepared = await this.#prepareDataRepresentation({
1364
- target: selection.target,
1365
- routedScheme: selection.scheme,
1366
- handler,
1367
- manifest: selection.manifest,
1368
- ctx,
1369
- schemeCtx,
1370
- publishedChannel: selection.channel,
1371
- });
1372
- if (prepared.result !== null)
1373
- return prepared.result;
1374
- if (prepared.address === null) {
1375
- return _a.#failure("entry-not-found", 404, `No entry exists at ${this.#resourceAddress(selection)}.`, {}, { target: this.#resourceAddress(selection) });
1376
- }
1377
- const storageAddress = prepared.address;
1378
- const read = await EntryCrud.readEntry(storageAddress.pathname, ctx, storageAddress.scheme, storageAddress.ownerId);
1379
- if (read.status >= 400)
1380
- return read;
1381
- if (read.status !== 200 || read.entry === null) {
1382
- throw new InvalidOperationResultError(`The '${selection.scheme}' scheme returned status ${read.status} without a COPY/MOVE source entry.`);
1383
- }
1384
- const selected = read.entry.channels[selection.channel];
1385
- if (selected === undefined) {
1386
- return _a.#failure("channel-not-found", 404, `No channel named #${selection.channel} exists at ${renderAddress(storageAddress.scheme, storageAddress.pathname)}.`, {}, {
1387
- target: renderAddress(storageAddress.scheme, storageAddress.pathname),
1388
- requestedChannel: selection.channel,
1389
- availableChannels: Object.keys(read.entry.channels),
1390
- retryable: false,
1391
- });
1392
- }
1393
- const resolvedMarker = this.#resolveResourceLineMarker(selection, selected.content, operation);
1394
- if ("result" in resolvedMarker)
1395
- return resolvedMarker.result;
1396
- let content = selected.content;
1397
- let scopeNormalizations;
1398
- if (await MimetypeBinary.isBinaryMimetype(selected.mimetype, ctx.mimetypes)) {
1399
- return _a.#failure("binary-source-unsupported", 415, `Channel #${selection.channel} is a binary marker and cannot be copied or moved.`, {}, {
1400
- channel: selection.channel,
1401
- mimetype: selected.mimetype,
1402
- recovery: "Use a source with a readable text projection.",
1403
- retryable: false,
1404
- });
1405
- }
1406
- if (resolvedMarker.selection.lineMarker !== null) {
1407
- const sliced = LineMarkerOps.sliceLinesRaw(content, resolvedMarker.selection.lineMarker);
1408
- if (sliced.status !== 200)
1409
- return Results.assert(sliced);
1410
- content = sliced.text ?? "";
1411
- scopeNormalizations = sliced.scopeNormalizations;
1412
- }
1413
- if (selected.producerResult !== undefined && selected.producerResult.status >= 400) {
1414
- return Results.assert(selected.producerResult);
1415
- }
1416
- return {
1417
- ...resolvedMarker.selection,
1418
- storageAddress,
1419
- content,
1420
- completeContent: selected.content,
1421
- mimetype: selected.mimetype,
1422
- lineAnchorPrecondition: resolvedMarker.precondition,
1423
- ...(scopeNormalizations === undefined ? {} : { scopeNormalizations }),
1424
- };
1425
- }
1426
- static #prependScopeNormalizations(result, scopeNormalizations) {
1427
- if (scopeNormalizations === undefined)
1428
- return result;
1429
- return Results.assert({
1430
- ...result,
1431
- scopeNormalizations: [
1432
- ...scopeNormalizations,
1433
- ...(result.scopeNormalizations ?? []),
1434
- ],
1435
- });
1436
- }
1437
- static #mergeLineAnchorPreconditions(...values) {
1438
- const present = values.filter((value) => value !== null);
1439
- if (present.length === 0)
1440
- return null;
1441
- const identity = present[0].identity;
1442
- if (present.some((value) => value.identity !== identity)) {
1443
- throw new TypeError("Line-anchor preconditions for one edit batch must share a resource identity.");
1444
- }
1445
- const checks = [...new Map(present.flatMap(({ checks: valueChecks }) => valueChecks)
1446
- .map((check) => [`${check.anchor}:${check.line}`, check])).values()];
1447
- return { identity, checks };
1448
- }
1449
- #resourceAddress(selection) {
1450
- const address = renderTarget({
1451
- scheme: selection.scheme === "file" ? null : selection.scheme,
1452
- pathname: selection.scheme === "file"
1453
- ? selection.identityPathname.replace(/^\//, "")
1454
- : selection.identityPathname,
1455
- fragment: selection.channel === selection.manifest.defaultChannel
1456
- ? null
1457
- : selection.channel,
1458
- });
1459
- if (address === null)
1460
- throw new Error("resolved resource selection has no renderable address");
1461
- return address;
1462
- }
1463
- #pendingEffect(selection, action) {
1464
- return {
1465
- target: this.#resourceAddress(selection),
1466
- action,
1467
- };
1468
- }
1469
- static #appliedEffects(result, pending) {
1470
- // {§edit-result-copy-move-effects} — only an applied mutation earns
1471
- // engine-composed effects; native scheme receipts remain internal input.
1472
- const exact = Results.assert(result);
1473
- if (exact.status === 304 || exact.status === 202 || exact.status >= 300)
1474
- return exact;
1475
- if (exact.effects !== undefined) {
1476
- throw new InvalidOperationResultError("A COPY/MOVE mutation result supplied effects before engine composition.");
1477
- }
1478
- const batch = exact.editReceipt === undefined
1479
- ? undefined
1480
- : assertEditBatchReceipt(exact.editReceipt);
1481
- const single = batch === undefined && exact.receipt !== undefined
1482
- ? assertEditReceipt(exact.receipt)
1483
- : undefined;
1484
- const batchSize = batch === undefined
1485
- ? undefined
1486
- : "disposition" in batch
1487
- ? batch.superseded.length
1488
- : batch.effects.length;
1489
- if (batchSize !== undefined && batchSize !== pending.length) {
1490
- throw new InvalidOperationResultError(`COPY/MOVE expected ${pending.length} receipt projections, got ${batchSize}.`);
1491
- }
1492
- if (single !== undefined && pending.length !== 1) {
1493
- throw new InvalidOperationResultError(`COPY/MOVE received one receipt for ${pending.length} resource effects.`);
1494
- }
1495
- let effects;
1496
- if (batch !== undefined && "disposition" in batch) {
1497
- const replacement = pending[0];
1498
- if (replacement === undefined) {
1499
- throw new InvalidOperationResultError("A reviewer-replaced COPY/MOVE batch has no resource effect.");
1500
- }
1501
- if (pending.some(({ target, action }) => target !== replacement.target || action !== replacement.action)) {
1502
- throw new InvalidOperationResultError("A reviewer-replaced COPY/MOVE batch spans incompatible resource effects.");
1503
- }
1504
- effects = [{
1505
- ...replacement,
1506
- receipt: projectEditReceipt(batch, 0),
1507
- }];
1508
- }
1509
- else {
1510
- effects = pending.map((effect, index) => ({
1511
- ...effect,
1512
- ...(batch !== undefined
1513
- ? { receipt: projectEditReceipt(batch, index) }
1514
- : single !== undefined
1515
- ? { receipt: single }
1516
- : {}),
1517
- }));
1518
- }
1519
- assertResourceEffects(effects);
1520
- const { editReceipt: _editReceipt, receipt: _receipt, ...withoutInternalReceipts } = exact;
1521
- return {
1522
- ...withoutInternalReceipts,
1523
- effects,
1524
- };
1525
- }
1526
- // {§copy-move-observation} {§edit-result-copy-move-effects} — scoped
1527
- // COPY/MOVE into an unscoped channel is a text materialization even when
1528
- // CRUD creates the channel wholesale. Carry the receipt through synchronous
1529
- // writes and proposals; the destination scheme owns reviewed output.
1530
- static #withEditMaterialization(result, receipt) {
1531
- const exact = Results.assert(result);
1532
- if (exact.status !== 200 && exact.status !== 201 && exact.status !== 202)
1533
- return exact;
1534
- const materialized = exact.editReceipt === undefined
1535
- ? assertEditBatchReceipt(receipt)
1536
- : assertEditBatchReceipt(exact.editReceipt);
1537
- return {
1538
- ...exact,
1539
- editReceipt: materialized,
1540
- ...(exact.status === 202
1541
- ? {
1542
- attrs: {
1543
- ...exact.attrs,
1544
- editReceipt: materialized,
1545
- },
1546
- }
1547
- : {}),
1548
- };
1549
- }
1550
- #finalizeEffects(result, selection, pending) {
1551
- const routed = this.#withProposalRoute(result, selection);
1552
- if (routed.status !== 202)
1553
- return _a.#appliedEffects(routed, pending);
1554
- return {
1555
- ...routed,
1556
- attrs: {
1557
- ...routed.attrs,
1558
- proposalEffects: pending,
1559
- },
1560
- };
1561
- }
1562
- static #effectsOf(result) {
1563
- return result.effects === undefined
1564
- ? []
1565
- : assertResourceEffects(result.effects);
1566
- }
1567
- static #withCombinedEffects(result, ...additional) {
1568
- const existing = _a.#effectsOf(result);
1569
- const effects = [...existing, ...additional.flat()];
1570
- const { effects: _effects, ...withoutEffects } = result;
1571
- return effects.length === 0
1572
- ? withoutEffects
1573
- : { ...withoutEffects, effects: assertResourceEffects(effects) };
1574
- }
1575
- static #settleProposalEffects(original, settlement) {
1576
- const pending = original.attrs
1577
- ?.proposalEffects;
1578
- if (pending === undefined
1579
- || settlement.resolution.decision !== "accept"
1580
- || settlement.applied === undefined
1581
- || settlement.applied.status >= 300) {
1582
- return settlement;
1583
- }
1584
- const projected = settlement.resolution.result ?? {};
1585
- const aggregate = settlement.applied.editReceipt;
1586
- const applied = _a.#appliedEffects({
1587
- ...projected,
1588
- status: settlement.applied.status,
1589
- ...(aggregate === undefined ? {} : { editReceipt: aggregate }),
1590
- }, pending);
1591
- const { status: _status, body: _body, ...result } = applied;
1592
- const { body: _resolutionBody, ...resolution } = settlement.resolution;
1593
- return {
1594
- ...settlement,
1595
- resolution: {
1596
- ...resolution,
1597
- result,
1598
- },
1599
- };
1600
- }
1601
- #recordEditSettlement(statement, settlement) {
1602
- if (statement.op !== "EDIT")
1603
- return;
1604
- const prepared = this.#preparedEdits.get(statement);
1605
- if (prepared === undefined || !prepared.first) {
1606
- throw new InvalidOperationResultError("An EDIT proposal settled without its prepared batch owner.");
1607
- }
1608
- const { resolution, applied } = settlement;
1609
- if (resolution.decision !== "accept"
1610
- || applied === undefined
1611
- || applied.status >= 300) {
1612
- prepared.batch.aggregate = undefined;
1613
- return;
1614
- }
1615
- if (applied.editReceipt === null) {
1616
- prepared.batch.aggregate = undefined;
1617
- return;
1618
- }
1619
- if (applied.editReceipt !== undefined) {
1620
- prepared.batch.aggregate = assertEditBatchReceipt(applied.editReceipt);
1621
- return;
1622
- }
1623
- if (resolution.body !== undefined)
1624
- prepared.batch.aggregate = undefined;
1625
- }
1626
- static #settlementEffects(settlement) {
1627
- const effects = settlement.resolution.result
1628
- ?.effects;
1629
- return effects === undefined ? [] : assertResourceEffects(effects);
1630
- }
1631
- static #withSettlementEffects(settlement, effects) {
1632
- const projected = (settlement.resolution.result ?? {});
1633
- const { effects: _effects, ...withoutEffects } = projected;
1634
- const result = effects.length === 0
1635
- ? withoutEffects
1636
- : {
1637
- ...withoutEffects,
1638
- effects: assertResourceEffects(effects),
1639
- };
1640
- return {
1641
- ...settlement,
1642
- resolution: {
1643
- ...settlement.resolution,
1644
- result,
1645
- },
1646
- };
1647
- }
1648
- #sameChannel(left, right) {
1649
- return left.scheme === right.scheme
1650
- && left.identityPathname === right.identityPathname
1651
- && left.channel === right.channel;
1652
- }
1653
- #withProposalRoute(result, selection) {
1654
- if (result.status !== 202)
1655
- return result;
1656
- return {
1657
- ...result,
1658
- attrs: {
1659
- ...result.attrs,
1660
- proposalScheme: selection.scheme,
1661
- proposalTarget: {
1662
- scheme: selection.scheme,
1663
- pathname: selection.identityPathname,
1664
- },
1665
- },
1666
- };
1667
- }
1668
- async #invokeEditBatch(selection, edits, ctx, precondition = null) {
1669
- const handler = this.#schemes.get(selection.scheme);
1670
- if (typeof handler?.editBatch !== "function") {
1671
- return _a.#failure("operation-not-implemented", 501, `Scheme '${selection.scheme}' does not implement EDIT batches.`, {}, {
1672
- scheme: selection.scheme,
1673
- operation: "EDIT",
1674
- retryable: false,
1675
- });
1676
- }
1677
- const statements = edits.map(({ marker, body, position }) => ({
1678
- op: "EDIT",
1679
- suffix: "",
1680
- signal: null,
1681
- target: selection.target,
1682
- lineMarker: marker,
1683
- body,
1684
- position,
1685
- }));
1686
- const addressedScheme = selection.target.kind === "url"
1687
- ? selection.target.scheme
1688
- : selection.scheme;
1689
- try {
1690
- const result = Results.assert(await handler.editBatch(statements, new SchemeCtxImpl(ctx, addressedScheme, selection.manifest, this.#liveSubscriptions, {
1691
- publishedChannel: selection.channel,
1692
- editPrecondition: precondition,
1693
- })));
1694
- return this.#withProposalRoute(result, selection);
1695
- }
1696
- catch (err) {
1697
- if (err instanceof InvalidOperationResultError)
1698
- throw err;
1699
- console.error(`Scheme '${selection.scheme}' COPY/MOVE edit threw outside its operation result contract:`, err);
1700
- return _a.#failure("scheme-handler-threw", 500, `The '${selection.scheme}' scheme did not produce a COPY/MOVE edit result.`, {}, {
1701
- stage: "scheme-dispatch",
1702
- scheme: selection.scheme,
1703
- operation: "EDIT",
1704
- });
1705
- }
1706
- }
1707
- async #writeDestination(statement, source, destination, ctx) {
1708
- const existingResult = await this.#readEntry(destination.scheme, destination.pathname, ctx);
1709
- if (existingResult.status >= 400 && existingResult.status !== 404) {
1710
- return existingResult;
1711
- }
1712
- const existing = existingResult.status === 200
1713
- ? existingResult.entry
1714
- : null;
1715
- if (existingResult.status === 200 && existing === null) {
1716
- throw new InvalidOperationResultError(`The '${destination.scheme}' scheme returned 200 without a destination entry.`);
1717
- }
1718
- const destinationChannel = existing?.channels[destination.channel];
1719
- const expectedMimetype = destinationChannel?.mimetype
1720
- ?? await PathMimetype.resolveEntryMimetype(destination.pathname, destination.manifest.channels[destination.channel] ?? source.mimetype, ctx.mimetypes);
1721
- if (source.mimetype !== expectedMimetype) {
1722
- return _a.#failure("mimetype-mismatch", 415, `COPY or MOVE cannot write '${source.mimetype}' into a '${expectedMimetype}' channel.`, {}, {
1723
- channel: destination.channel,
1724
- sourceMimetype: source.mimetype,
1725
- destinationMimetype: expectedMimetype,
1726
- retryable: false,
1727
- });
1728
- }
1729
- const destinationEffect = this.#pendingEffect(destination, destinationChannel === undefined ? "create" : "update");
1730
- if (destination.lineMarker !== null) {
1731
- if (destinationChannel === undefined) {
1732
- return _a.#failure("destination-region-not-found", 404, `A destination region requires an existing #${destination.channel} channel.`, {}, {
1733
- destination: this.#resourceAddress(destination),
1734
- retryable: false,
1735
- });
1736
- }
1737
- if (await MimetypeBinary.isBinaryMimetype(destinationChannel.mimetype, ctx.mimetypes)) {
1738
- return _a.#failure("binary-region-unsupported", 415, `Channel #${destination.channel} is binary and cannot receive a textual region.`, {}, {
1739
- channel: destination.channel,
1740
- mimetype: destinationChannel.mimetype,
1741
- retryable: false,
1742
- });
1743
- }
1744
- const resolvedMarker = this.#resolveResourceLineMarker(destination, destinationChannel.content, statement.op);
1745
- if ("result" in resolvedMarker)
1746
- return resolvedMarker.result;
1747
- const edited = await this.#invokeEditBatch(resolvedMarker.selection, [{
1748
- marker: resolvedMarker.selection.lineMarker,
1749
- body: source.content,
1750
- position: statement.position,
1751
- }], ctx, resolvedMarker.precondition);
1752
- return this.#finalizeEffects(edited, resolvedMarker.selection, [destinationEffect]);
1753
- }
1754
- if (destinationChannel !== undefined
1755
- && destinationChannel.content !== source.content) {
1756
- return _a.#failure("copy-destination-exists", 409, `COPY or MOVE destination ${this.#resourceAddress(destination)} already contains different content.`, {}, {
1757
- destination: this.#resourceAddress(destination),
1758
- retryable: false,
1759
- });
1760
- }
1761
- if (destinationChannel !== undefined)
1762
- return { status: 304 };
1763
- const channels = {
1764
- ...(existing?.channels ?? {}),
1765
- [destination.channel]: {
1766
- content: source.content,
1767
- mimetype: source.mimetype,
1768
- },
1769
- };
1770
- const written = await this.#writeEntry(destination.scheme, destination.pathname, { channels }, ctx);
1771
- const exactWritten = Results.assert(written);
1772
- const parseIssues = exactWritten.status === 200 || exactWritten.status === 201
1773
- ? await new DbProjectionCaps(ctx).parseIssues(source.content, source.mimetype)
1774
- : undefined;
1775
- const materialized = source.lineMarker === null
1776
- || (exactWritten.status !== 200 && exactWritten.status !== 201 && exactWritten.status !== 202)
1777
- ? exactWritten
1778
- : _a.#withEditMaterialization(exactWritten, editReceipt("", source.content, [{
1779
- marker: { marks: [1, -1] },
1780
- body: source.content,
1781
- }], parseIssues));
1782
- return this.#finalizeEffects(materialized, destination, [destinationEffect]);
1783
- }
1784
- async #copyOrchestration({ statement, source, destination, ctx, }) {
1785
- const resolvedSource = await this.#resolveResourceSelection(source, ctx);
1786
- if (_a.#isDispatchResult(resolvedSource))
1787
- return resolvedSource;
1788
- const resolvedDestination = await this.#resolveResourceSelection(destination, ctx);
1789
- if (_a.#isDispatchResult(resolvedDestination))
1790
- return resolvedDestination;
1791
- const selected = await this.#selectSource(resolvedSource, ctx, "COPY");
1792
- if (_a.#isDispatchResult(selected))
1793
- return selected;
1794
- const result = await this.#writeDestination(statement, selected, resolvedDestination, ctx);
1795
- return _a.#prependScopeNormalizations(result, selected.scopeNormalizations);
1796
- }
1797
- #deferredMoveSource(source, destination, lineAnchorPrecondition) {
1798
- return {
1799
- target: source.target,
1800
- lineMarker: source.lineMarker,
1801
- scheme: source.scheme,
1802
- pathname: source.pathname,
1803
- channel: source.channel,
1804
- destination: this.#resourceAddress(destination),
1805
- lineAnchorPrecondition,
1806
- };
1807
- }
1808
- #moveFailureAfterDestination(result, destination, destinationEffects) {
1809
- const exact = Results.assert(result);
1810
- if (exact.status < 400) {
1811
- throw new InvalidOperationResultError("A successful MOVE source result was classified as a partial failure.");
1812
- }
1813
- if (exact.problem === undefined) {
1814
- throw new InvalidOperationResultError("A failed MOVE source mutation has no Problem Details.");
1815
- }
1816
- const failed = Results.assert({
1817
- ...exact,
1818
- problem: {
1819
- ...exact.problem,
1820
- operation: "MOVE",
1821
- destinationWritten: true,
1822
- destination,
1823
- },
1824
- });
1825
- return _a.#withCombinedEffects(failed, destinationEffects);
1826
- }
1827
- async #removeMoveSource(statement, source, ctx, lineAnchorPrecondition = null) {
1828
- const effect = this.#pendingEffect(source, source.lineMarker === null ? "delete" : "update");
1829
- if (source.lineMarker === null) {
1830
- const deleted = await this.#deleteChannel(source.scheme, source.pathname, source.channel, ctx);
1831
- return this.#finalizeEffects(Results.assert(deleted), source, [effect]);
1832
- }
1833
- const edited = await this.#invokeEditBatch(source, [{
1834
- marker: source.lineMarker,
1835
- body: "",
1836
- position: statement.position,
1837
- }], ctx, lineAnchorPrecondition);
1838
- return this.#finalizeEffects(edited, source, [effect]);
1839
- }
1840
- async #moveWithinChannel(statement, source, destination, ctx) {
1841
- if (source.lineMarker === null) {
1842
- if (destination.lineMarker !== null) {
1843
- return _a.#failure("move-region-overlap", 409, "MOVE cannot insert a whole channel into itself and then remove that channel.", {}, {
1844
- source: this.#resourceAddress(source),
1845
- destination: this.#resourceAddress(destination),
1846
- retryable: false,
1847
- });
1848
- }
1849
- return this.#writeDestination(statement, source, destination, ctx);
1850
- }
1851
- if (destination.lineMarker === null) {
1852
- return this.#writeDestination(statement, source, destination, ctx);
1853
- }
1854
- const resolvedDestination = this.#resolveResourceLineMarker(destination, source.completeContent, "MOVE");
1855
- if ("result" in resolvedDestination)
1856
- return resolvedDestination.result;
1857
- const precondition = _a.#mergeLineAnchorPreconditions(source.lineAnchorPrecondition, resolvedDestination.precondition);
1858
- const moved = await this.#invokeEditBatch(resolvedDestination.selection, [
1859
- {
1860
- marker: resolvedDestination.selection.lineMarker,
1861
- body: source.content,
1862
- position: statement.position,
1863
- },
1864
- {
1865
- marker: source.lineMarker,
1866
- body: "",
1867
- position: statement.position,
1868
- },
1869
- ], ctx, precondition);
1870
- const effect = this.#pendingEffect(resolvedDestination.selection, "update");
1871
- return this.#finalizeEffects(moved, resolvedDestination.selection, [effect, effect]);
1872
- }
1873
- async #moveOrchestration({ statement, source, destination, ctx, }) {
1874
- const resolvedSource = await this.#resolveResourceSelection(source, ctx);
1875
- if (_a.#isDispatchResult(resolvedSource))
1876
- return resolvedSource;
1877
- const resolvedDestination = await this.#resolveResourceSelection(destination, ctx);
1878
- if (_a.#isDispatchResult(resolvedDestination))
1879
- return resolvedDestination;
1880
- const selected = await this.#selectSource(resolvedSource, ctx, "MOVE");
1881
- if (_a.#isDispatchResult(selected))
1882
- return selected;
1883
- if (this.#sameChannel(resolvedSource, resolvedDestination)) {
1884
- const result = await this.#moveWithinChannel(statement, selected, resolvedDestination, ctx);
1885
- return resolvedDestination.lineMarker === null
1886
- ? _a.#prependScopeNormalizations(result, selected.scopeNormalizations)
1887
- : result;
1888
- }
1889
- const destinationResult = _a.#prependScopeNormalizations(await this.#writeDestination(statement, selected, resolvedDestination, ctx), selected.scopeNormalizations);
1890
- if (destinationResult.status >= 400)
1891
- return destinationResult;
1892
- const destinationAddress = this.#resourceAddress(resolvedDestination);
1893
- const destinationEffects = _a.#effectsOf(destinationResult);
1894
- if (destinationResult.status === 202) {
1895
- return {
1896
- ...destinationResult,
1897
- attrs: {
1898
- ...destinationResult.attrs,
1899
- moveSource: this.#deferredMoveSource(selected, resolvedDestination, selected.lineAnchorPrecondition),
1900
- },
1901
- };
1902
- }
1903
- const sourceResult = await this.#removeMoveSource(statement, selected, ctx, selected.lineAnchorPrecondition);
1904
- if (sourceResult.status === 202) {
1905
- return {
1906
- ...sourceResult,
1907
- ...(destinationResult.scopeNormalizations === undefined
1908
- ? {}
1909
- : { scopeNormalizations: destinationResult.scopeNormalizations }),
1910
- attrs: {
1911
- ...sourceResult.attrs,
1912
- moveDestinationWritten: destinationAddress,
1913
- moveDestinationEffects: destinationEffects,
1914
- },
1915
- };
1916
- }
1917
- if (sourceResult.status >= 400) {
1918
- return this.#moveFailureAfterDestination(destinationResult.scopeNormalizations === undefined
1919
- ? sourceResult
1920
- : Results.assert({
1921
- ...sourceResult,
1922
- scopeNormalizations: destinationResult.scopeNormalizations,
1923
- }), destinationAddress, destinationEffects);
1924
- }
1925
- const base = destinationResult.status === 304
1926
- ? { ...destinationResult, status: 200 }
1927
- : destinationResult;
1928
- return _a.#withCombinedEffects(base, _a.#effectsOf(sourceResult));
1929
- }
1930
- async #settleMoveProposal({ statement, result, settlement, ctx, ids, }) {
1931
- if (statement.op !== "MOVE")
1932
- return settlement;
1933
- const attrs = result.attrs;
1934
- const destinationWritten = attrs?.moveDestinationWritten;
1935
- if (destinationWritten !== undefined) {
1936
- const destinationEffects = attrs?.moveDestinationEffects === undefined
1937
- ? []
1938
- : assertResourceEffects(attrs.moveDestinationEffects);
1939
- if (settlement.resolution.decision !== "accept") {
1940
- const decision = settlement.resolution.decision;
1941
- return {
1942
- resolution: settlement.resolution,
1943
- applied: this.#moveFailureAfterDestination(_a.#failure("move-source-not-applied", decision === "cancel" ? 499 : 409, `The MOVE destination was written, but source removal was ${decision === "cancel" ? "cancelled" : "rejected"}.`, {}, {
1944
- retryable: false,
1945
- }), destinationWritten, destinationEffects),
1946
- };
1947
- }
1948
- if (settlement.applied === undefined) {
1949
- return {
1950
- resolution: settlement.resolution,
1951
- applied: this.#moveFailureAfterDestination(_a.#failure("proposal-apply-missing", 500, "The source scheme accepted its MOVE proposal without applying the source mutation.", {}, {
1952
- stage: "proposal-application",
1953
- retryable: false,
1954
- }), destinationWritten, destinationEffects),
1955
- };
1956
- }
1957
- if (settlement.applied.status >= 400) {
1958
- return {
1959
- resolution: settlement.resolution,
1960
- applied: this.#moveFailureAfterDestination(settlement.applied, destinationWritten, destinationEffects),
1961
- };
1962
- }
1963
- return _a.#withSettlementEffects(settlement, [
1964
- ...destinationEffects,
1965
- ..._a.#settlementEffects(settlement),
1966
- ]);
1967
- }
1968
- const deferred = attrs?.moveSource;
1969
- if (deferred === undefined
1970
- || settlement.resolution.decision !== "accept"
1971
- || (settlement.applied?.status ?? 200) >= 400) {
1972
- return settlement;
1973
- }
1974
- if (settlement.applied === undefined) {
1975
- return {
1976
- resolution: settlement.resolution,
1977
- applied: _a.#failure("proposal-apply-missing", 500, "The destination scheme accepted its MOVE proposal without applying the destination mutation.", {}, {
1978
- stage: "proposal-application",
1979
- retryable: false,
1980
- }),
1981
- };
1982
- }
1983
- const destinationEffects = _a.#settlementEffects(settlement);
1984
- const resolvedSource = await this.#resolveResourceSelection({
1985
- target: deferred.target,
1986
- lineMarker: deferred.lineMarker,
1987
- }, ctx);
1988
- if (_a.#isDispatchResult(resolvedSource)) {
1989
- return {
1990
- resolution: settlement.resolution,
1991
- applied: this.#moveFailureAfterDestination(resolvedSource, deferred.destination, destinationEffects),
1992
- };
1993
- }
1994
- if (resolvedSource.scheme !== deferred.scheme
1995
- || resolvedSource.pathname !== deferred.pathname
1996
- || resolvedSource.channel !== deferred.channel) {
1997
- throw new InvalidOperationResultError("A deferred MOVE source no longer resolves to its recorded identity.");
1998
- }
1999
- const removed = await this.#removeMoveSource(statement, {
2000
- ...resolvedSource,
2001
- lineMarker: resolvedSource.lineMarker,
2002
- }, ctx, deferred.lineAnchorPrecondition);
2003
- if (removed.status >= 400) {
2004
- return {
2005
- resolution: settlement.resolution,
2006
- applied: this.#moveFailureAfterDestination(removed, deferred.destination, destinationEffects),
2007
- };
2008
- }
2009
- if (removed.status !== 202) {
2010
- return _a.#withSettlementEffects(settlement, [
2011
- ...destinationEffects,
2012
- ..._a.#effectsOf(removed),
2013
- ]);
2014
- }
2015
- const initialSourceSettlement = await this.#proposals.workerApply(statement, removed, { decision: "accept" }, ids);
2016
- const sourceSettlement = _a.#settleProposalEffects(removed, initialSourceSettlement);
2017
- if (sourceSettlement.applied === undefined) {
2018
- return {
2019
- resolution: settlement.resolution,
2020
- applied: this.#moveFailureAfterDestination(_a.#failure("proposal-apply-missing", 500, "The source scheme accepted its MOVE proposal without applying the source mutation.", {}, {
2021
- stage: "proposal-application",
2022
- retryable: false,
2023
- }), deferred.destination, destinationEffects),
2024
- };
2025
- }
2026
- if (sourceSettlement.applied.status >= 400) {
2027
- return {
2028
- resolution: settlement.resolution,
2029
- applied: this.#moveFailureAfterDestination(sourceSettlement.applied, deferred.destination, destinationEffects),
2030
- };
2031
- }
2032
- return _a.#withSettlementEffects(settlement, [
2033
- ...destinationEffects,
2034
- ..._a.#settlementEffects(sourceSettlement),
2035
- ]);
2036
- }
2037
997
  // {§send-premature-terminate} — the unified PENDING SET, judged at the terminal's OWN dispatch
2038
998
  // (post-batch: the emission's earlier ops already executed, so a same-turn KILL+[200] repairs in
2039
999
  // ONE turn, and a same-turn WORK+[200] is caught — the spawn is live by the time the SEND lands).
@@ -2262,8 +1222,8 @@ class Dispatcher {
2262
1222
  content: null,
2263
1223
  mimetype: null,
2264
1224
  results: [],
2265
- itemsTokenTotal: 0,
2266
- returnedItemsTokenTotal: 0,
1225
+ itemsWeightTotal: 0,
1226
+ returnedItemsWeightTotal: 0,
2267
1227
  matchingPathCount: 0,
2268
1228
  matchLocationCount: 0,
2269
1229
  }
@@ -2292,14 +1252,14 @@ class Dispatcher {
2292
1252
  }
2293
1253
  }
2294
1254
  }
2295
- const handler = this.#schemes.get(schemeName);
1255
+ const handler = this.#schemes.get(schemeName, ctx.workspaceId);
2296
1256
  if (handler === undefined) {
2297
1257
  return _a.#failure("scheme-not-found", 501, `Scheme '${schemeName}' is not registered.`, {}, { scheme: schemeName, retryable: false });
2298
1258
  }
2299
1259
  const methodName = statement.op.toLowerCase();
2300
1260
  const method = handler[methodName];
2301
1261
  const addressedScheme = statement.target?.kind === "url" ? statement.target.scheme : null;
2302
- const manifest = this.#schemes.manifestFor(schemeName);
1262
+ const manifest = this.#schemes.manifestFor(schemeName, ctx.workspaceId);
2303
1263
  if (manifest === undefined)
2304
1264
  throw new Error(`scheme '${schemeName}' has no manifest`);
2305
1265
  const publishedChannel = statement.target?.kind === "url"
@@ -2406,8 +1366,8 @@ class Dispatcher {
2406
1366
  content: null,
2407
1367
  mimetype: null,
2408
1368
  results: [],
2409
- itemsTokenTotal: 0,
2410
- returnedItemsTokenTotal: 0,
1369
+ itemsWeightTotal: 0,
1370
+ returnedItemsWeightTotal: 0,
2411
1371
  matchingPathCount: 0,
2412
1372
  matchLocationCount: 0,
2413
1373
  }, { target });
@@ -2465,7 +1425,7 @@ class Dispatcher {
2465
1425
  }
2466
1426
  async #writeLog({ statement, result, workspaceId, workerId, loopId, turnId, sequence, origin, curationPlan, modelCallId, }) {
2467
1427
  const durableStatement = DurableStatement.project(statement);
2468
- const target = this.#extractTarget(durableStatement.target);
1428
+ const target = this.#extractTarget(durableStatement.target, workspaceId);
2469
1429
  await this.#canonColumns(target, workspaceId); // {§fs-answer-in-canon}
2470
1430
  const lineMarkerJson = "lineMarker" in durableStatement && durableStatement.lineMarker !== null
2471
1431
  ? JSON.stringify(durableStatement.lineMarker)
@@ -2558,7 +1518,14 @@ class Dispatcher {
2558
1518
  rx: rxJson,
2559
1519
  mimetype_rx: "application/json",
2560
1520
  status_rx: result.status,
2561
- tokens: this.#tokenize(txJson) + this.#tokenize(rxJson),
1521
+ weight: LogBody.weight({
1522
+ op: durableStatement.op,
1523
+ attrs,
1524
+ tx: txJson,
1525
+ rx: rxJson,
1526
+ mimetypeTx: "application/json",
1527
+ mimetypeRx: "application/json",
1528
+ }, this.#weighContent),
2562
1529
  state: isProposed ? "proposed" : "resolved",
2563
1530
  outcome: null,
2564
1531
  attrs,
@@ -2571,7 +1538,7 @@ class Dispatcher {
2571
1538
  // inputs collapse to scheme=null in log target metadata because both render
2572
1539
  // as bare paths. Addressable file entries separately persist under the
2573
1540
  // reserved `file` identity scheme ({§entry-identity-no-null}).
2574
- #extractTarget(path) {
1541
+ #extractTarget(path, workspaceId) {
2575
1542
  if (path === null)
2576
1543
  return { scheme: null, username: null, password: null, hostname: null, port: null, pathname: null, query: null, fragment: null };
2577
1544
  // `local` (bare path) carries no URL parts — store the raw text as the pathname for the log record, scheme=null.
@@ -2584,7 +1551,7 @@ class Dispatcher {
2584
1551
  const foldNs = scheme !== null
2585
1552
  && scheme !== "worker"
2586
1553
  && !NetworkAddress.supports(scheme)
2587
- && this.#schemes.has(scheme);
1554
+ && this.#schemes.has(scheme, workspaceId);
2588
1555
  return {
2589
1556
  scheme, username: path.username, password: path.password,
2590
1557
  hostname: foldNs ? null : path.hostname, port: path.port,