@plurnk/plurnk-service 1.5.0 → 1.6.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 (201) hide show
  1. package/.env.defaults +21 -12
  2. package/INSTALL.md +11 -7
  3. package/SPEC.md +437 -304
  4. package/dist/build-info.json +1 -1
  5. package/dist/content/edit-collision.d.ts +5 -0
  6. package/dist/content/edit-collision.d.ts.map +1 -0
  7. package/dist/content/edit-collision.js +14 -0
  8. package/dist/content/edit-collision.js.map +1 -0
  9. package/dist/content/edit-receipt.d.ts +3 -2
  10. package/dist/content/edit-receipt.d.ts.map +1 -1
  11. package/dist/content/edit-receipt.js +58 -13
  12. package/dist/content/edit-receipt.js.map +1 -1
  13. package/dist/content/index.d.ts +4 -1
  14. package/dist/content/index.d.ts.map +1 -1
  15. package/dist/content/index.js +3 -1
  16. package/dist/content/index.js.map +1 -1
  17. package/dist/content/line-anchors.d.ts +41 -0
  18. package/dist/content/line-anchors.d.ts.map +1 -0
  19. package/dist/content/line-anchors.js +191 -0
  20. package/dist/content/line-anchors.js.map +1 -0
  21. package/dist/content/read-projector.d.ts +7 -1
  22. package/dist/content/read-projector.d.ts.map +1 -1
  23. package/dist/content/read-projector.js +46 -10
  24. package/dist/content/read-projector.js.map +1 -1
  25. package/dist/content/read-resolve.js +3 -3
  26. package/dist/content/read-resolve.js.map +1 -1
  27. package/dist/core/BudgetReadout.d.ts.map +1 -1
  28. package/dist/core/BudgetReadout.js +9 -2
  29. package/dist/core/BudgetReadout.js.map +1 -1
  30. package/dist/core/ChannelWrite.js +1 -1
  31. package/dist/core/ChannelWrite.js.map +1 -1
  32. package/dist/core/Dispatcher.d.ts +13 -3
  33. package/dist/core/Dispatcher.d.ts.map +1 -1
  34. package/dist/core/Dispatcher.js +391 -113
  35. package/dist/core/Dispatcher.js.map +1 -1
  36. package/dist/core/DurableStatement.d.ts.map +1 -1
  37. package/dist/core/DurableStatement.js +2 -0
  38. package/dist/core/DurableStatement.js.map +1 -1
  39. package/dist/core/Engine.d.ts +13 -14
  40. package/dist/core/Engine.d.ts.map +1 -1
  41. package/dist/core/Engine.js +434 -300
  42. package/dist/core/Engine.js.map +1 -1
  43. package/dist/core/Engine.sql +156 -90
  44. package/dist/core/ExecutableTools.d.ts +10 -0
  45. package/dist/core/ExecutableTools.d.ts.map +1 -0
  46. package/dist/core/ExecutableTools.js +41 -0
  47. package/dist/core/ExecutableTools.js.map +1 -0
  48. package/dist/core/ExecutorRegistry.d.ts +3 -3
  49. package/dist/core/ExecutorRegistry.d.ts.map +1 -1
  50. package/dist/core/ExecutorRegistry.js +2 -2
  51. package/dist/core/ExecutorRegistry.js.map +1 -1
  52. package/dist/core/LogBody.d.ts +2 -0
  53. package/dist/core/LogBody.d.ts.map +1 -1
  54. package/dist/core/LogBody.js +17 -5
  55. package/dist/core/LogBody.js.map +1 -1
  56. package/dist/core/LogEntryProjection.d.ts +14 -0
  57. package/dist/core/LogEntryProjection.d.ts.map +1 -0
  58. package/dist/core/LogEntryProjection.js +38 -0
  59. package/dist/core/LogEntryProjection.js.map +1 -0
  60. package/dist/core/ModelCall.d.ts +27 -0
  61. package/dist/core/ModelCall.d.ts.map +1 -0
  62. package/dist/core/ModelCall.js +125 -0
  63. package/dist/core/ModelCall.js.map +1 -0
  64. package/dist/core/PacketBuilder.d.ts +10 -10
  65. package/dist/core/PacketBuilder.d.ts.map +1 -1
  66. package/dist/core/PacketBuilder.js +131 -103
  67. package/dist/core/PacketBuilder.js.map +1 -1
  68. package/dist/core/ProblemLog.d.ts.map +1 -1
  69. package/dist/core/ProblemLog.js +1 -0
  70. package/dist/core/ProblemLog.js.map +1 -1
  71. package/dist/core/ProposalLifecycle.d.ts.map +1 -1
  72. package/dist/core/ProposalLifecycle.js +3 -4
  73. package/dist/core/ProposalLifecycle.js.map +1 -1
  74. package/dist/core/ProviderInstantiate.d.ts.map +1 -1
  75. package/dist/core/ProviderInstantiate.js +23 -6
  76. package/dist/core/ProviderInstantiate.js.map +1 -1
  77. package/dist/core/SchemeRegistry.js +7 -7
  78. package/dist/core/SchemeRegistry.js.map +1 -1
  79. package/dist/core/StrikeRail.d.ts +0 -1
  80. package/dist/core/StrikeRail.d.ts.map +1 -1
  81. package/dist/core/StrikeRail.js +6 -4
  82. package/dist/core/StrikeRail.js.map +1 -1
  83. package/dist/core/caps/CapsResolve.js +1 -1
  84. package/dist/core/caps/CapsResolve.js.map +1 -1
  85. package/dist/core/caps/DbEntryCaps.d.ts +2 -1
  86. package/dist/core/caps/DbEntryCaps.d.ts.map +1 -1
  87. package/dist/core/caps/DbEntryCaps.js +8 -5
  88. package/dist/core/caps/DbEntryCaps.js.map +1 -1
  89. package/dist/core/caps/DbProjectionCaps.d.ts +1 -0
  90. package/dist/core/caps/DbProjectionCaps.d.ts.map +1 -1
  91. package/dist/core/caps/DbProjectionCaps.js +29 -0
  92. package/dist/core/caps/DbProjectionCaps.js.map +1 -1
  93. package/dist/core/caps/SchemeCtxImpl.d.ts +5 -2
  94. package/dist/core/caps/SchemeCtxImpl.d.ts.map +1 -1
  95. package/dist/core/caps/SchemeCtxImpl.js +6 -4
  96. package/dist/core/caps/SchemeCtxImpl.js.map +1 -1
  97. package/dist/core/fork.d.ts.map +1 -1
  98. package/dist/core/fork.js +23 -7
  99. package/dist/core/fork.js.map +1 -1
  100. package/dist/core/fork.sql +33 -12
  101. package/dist/core/git-membership.d.ts.map +1 -1
  102. package/dist/core/git-membership.js +1 -2
  103. package/dist/core/git-membership.js.map +1 -1
  104. package/dist/core/git-state.js +1 -1
  105. package/dist/core/git-state.js.map +1 -1
  106. package/dist/core/packet-wire.d.ts +4 -1
  107. package/dist/core/packet-wire.d.ts.map +1 -1
  108. package/dist/core/packet-wire.js +248 -90
  109. package/dist/core/packet-wire.js.map +1 -1
  110. package/dist/core/provider-accounting.d.ts +24 -0
  111. package/dist/core/provider-accounting.d.ts.map +1 -0
  112. package/dist/core/provider-accounting.js +76 -0
  113. package/dist/core/provider-accounting.js.map +1 -0
  114. package/dist/core/search-gate.d.ts +2 -2
  115. package/dist/core/search-gate.d.ts.map +1 -1
  116. package/dist/core/search-gate.js +8 -8
  117. package/dist/core/search-gate.js.map +1 -1
  118. package/dist/core/token-ruler.js +1 -1
  119. package/dist/core/token-ruler.js.map +1 -1
  120. package/dist/core/turn-scheduler.js +1 -1
  121. package/dist/core/turn-scheduler.js.map +1 -1
  122. package/dist/digest/Digest.d.ts.map +1 -1
  123. package/dist/digest/Digest.js +220 -109
  124. package/dist/digest/Digest.js.map +1 -1
  125. package/dist/digest/digest.sql +40 -18
  126. package/dist/schemes/Exec.d.ts.map +1 -1
  127. package/dist/schemes/Exec.js +111 -101
  128. package/dist/schemes/Exec.js.map +1 -1
  129. package/dist/schemes/File.d.ts +5 -4
  130. package/dist/schemes/File.d.ts.map +1 -1
  131. package/dist/schemes/File.js +24 -22
  132. package/dist/schemes/File.js.map +1 -1
  133. package/dist/schemes/Log.d.ts +11 -0
  134. package/dist/schemes/Log.d.ts.map +1 -1
  135. package/dist/schemes/Log.js +171 -106
  136. package/dist/schemes/Log.js.map +1 -1
  137. package/dist/schemes/Log.sql +48 -32
  138. package/dist/schemes/Prompt.d.ts +3 -3
  139. package/dist/schemes/Prompt.d.ts.map +1 -1
  140. package/dist/schemes/Prompt.js +4 -1
  141. package/dist/schemes/Prompt.js.map +1 -1
  142. package/dist/schemes/Skill.d.ts +4 -4
  143. package/dist/schemes/Skill.d.ts.map +1 -1
  144. package/dist/schemes/Skill.js +3 -0
  145. package/dist/schemes/Skill.js.map +1 -1
  146. package/dist/schemes/Worker.d.ts +4 -4
  147. package/dist/schemes/Worker.d.ts.map +1 -1
  148. package/dist/schemes/Worker.js +9 -5
  149. package/dist/schemes/Worker.js.map +1 -1
  150. package/dist/schemes/_entry-crud.d.ts +0 -1
  151. package/dist/schemes/_entry-crud.d.ts.map +1 -1
  152. package/dist/schemes/_entry-crud.js +15 -9
  153. package/dist/schemes/_entry-crud.js.map +1 -1
  154. package/dist/schemes/_entry-crud.sql +2 -14
  155. package/dist/schemes/_entry-find.d.ts +13 -12
  156. package/dist/schemes/_entry-find.d.ts.map +1 -1
  157. package/dist/schemes/_entry-find.js +33 -28
  158. package/dist/schemes/_entry-find.js.map +1 -1
  159. package/dist/schemes/_entry-find.sql +2 -21
  160. package/dist/schemes/_entry-graph.sql +1 -1
  161. package/dist/schemes/_entry-manifest.d.ts +5 -17
  162. package/dist/schemes/_entry-manifest.d.ts.map +1 -1
  163. package/dist/schemes/_entry-manifest.js +28 -24
  164. package/dist/schemes/_entry-manifest.js.map +1 -1
  165. package/dist/schemes/_entry-ops.d.ts +5 -4
  166. package/dist/schemes/_entry-ops.d.ts.map +1 -1
  167. package/dist/schemes/_entry-ops.js +87 -47
  168. package/dist/schemes/_entry-ops.js.map +1 -1
  169. package/dist/schemes/_entry-ops.sql +30 -4
  170. package/dist/schemes/_entry-send.js +4 -4
  171. package/dist/schemes/_entry-send.js.map +1 -1
  172. package/dist/schemes/_search-index.d.ts.map +1 -1
  173. package/dist/schemes/_search-index.js +10 -2
  174. package/dist/schemes/_search-index.js.map +1 -1
  175. package/dist/schemes/exec-abort.js +2 -2
  176. package/dist/schemes/exec-abort.js.map +1 -1
  177. package/dist/server/Daemon.d.ts +1 -1
  178. package/dist/server/Daemon.d.ts.map +1 -1
  179. package/dist/server/Daemon.js +25 -23
  180. package/dist/server/Daemon.js.map +1 -1
  181. package/dist/server/drain.sql +2 -2
  182. package/dist/server/envelope.d.ts +0 -2
  183. package/dist/server/envelope.d.ts.map +1 -1
  184. package/dist/server/envelope.js.map +1 -1
  185. package/dist/server/envelope.sql +2 -2
  186. package/dist/server/lifecycle-recovery.sql +25 -15
  187. package/dist/server/logEntry.d.ts +1 -0
  188. package/dist/server/logEntry.d.ts.map +1 -1
  189. package/dist/server/logEntry.js +13 -1
  190. package/dist/server/logEntry.js.map +1 -1
  191. package/dist/server/logEntry.sql +5 -1
  192. package/migrations/001_schema.sql +603 -207
  193. package/package.json +39 -34
  194. package/dist/core/BudgetOverflow.d.ts +0 -20
  195. package/dist/core/BudgetOverflow.d.ts.map +0 -1
  196. package/dist/core/BudgetOverflow.js +0 -49
  197. package/dist/core/BudgetOverflow.js.map +0 -1
  198. package/dist/core/caps/DbTagCaps.d.ts +0 -10
  199. package/dist/core/caps/DbTagCaps.d.ts.map +0 -1
  200. package/dist/core/caps/DbTagCaps.js +0 -46
  201. package/dist/core/caps/DbTagCaps.js.map +0 -1
@@ -5,11 +5,12 @@ import WorkerControlAddress from "./WorkerControlAddress.js";
5
5
  import { entryPathnameOf, foldAuthorityIntoPath, renderAddress, renderTarget, schemeNameOf } from "./plurnk-uri.js";
6
6
  import Fork from "./fork.js";
7
7
  import WorkerCap from "./worker-cap.js";
8
- import { PathSyntax } from "@plurnk/plurnk-contracts";
8
+ 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, editReceipt, LineMarkerOps, MimetypeBinary, PathMimetype, ReadProjector, projectEditReceipt, } from "../content/index.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";
13
14
  import SchemeCtxImpl from "./caps/SchemeCtxImpl.js";
14
15
  import EntryCrud from "../schemes/_entry-crud.js";
15
16
  import EntryOps from "../schemes/_entry-ops.js";
@@ -26,6 +27,16 @@ import DurableStatement from "./DurableStatement.js";
26
27
  // SPEC {§scheme-surface}: writer must be in target scheme's manifest.writableBy.
27
28
  // OPEN/FOLD/READ/FIND are not gated — they curate the log or read, never mutating an entry.
28
29
  const MUTATING_OPS = new Set(["EDIT", "SEND", "COPY", "MOVE", "EXEC", "KILL", "FORK", "WORK"]);
30
+ const assertClassifyingSignal = (statement) => {
31
+ if (statement.op === "FIND"
32
+ || statement.op === "READ"
33
+ || statement.op === "EDIT"
34
+ || statement.op === "COPY"
35
+ || statement.op === "MOVE"
36
+ || statement.op === "BARE") {
37
+ TagSignal.applied(statement.signal);
38
+ }
39
+ };
29
40
  // Op dispatch ({§op-methods-op-dispatch}): gates (writableBy, loop flags), the
30
41
  // engine-owned op orchestrations (COPY/MOVE/KILL/SEND), scheme
31
42
  // routing, the durable log write, and the proposal pause.
@@ -55,7 +66,7 @@ class Dispatcher {
55
66
  #branchCompletionGate;
56
67
  #cancelWorker;
57
68
  #cancelDescendants;
58
- // {§send-premature-terminate}/SEND[202]<T> — the engine-owned park-deadline registry (loopId → seconds;
69
+ // {§send-premature-terminate}/SEND signal 202 with scope — the engine-owned park-deadline registry (loopId → seconds;
59
70
  // -1 = indefinite). The dispatcher WRITES at park; the daemon's drain park-exit consumes.
60
71
  #parkDeadlines;
61
72
  #searchGate;
@@ -124,7 +135,12 @@ class Dispatcher {
124
135
  status: result.status,
125
136
  entry: result.entry === null
126
137
  ? null
127
- : { channels: { ...result.entry.channels }, tags: [...result.entry.tags] },
138
+ : {
139
+ channels: { ...result.entry.channels },
140
+ ...(result.entry.attributes === undefined
141
+ ? {}
142
+ : { attributes: { ...result.entry.attributes } }),
143
+ },
128
144
  });
129
145
  }
130
146
  async #writeEntry(scheme, pathname, entry, ctx) {
@@ -197,11 +213,114 @@ class Dispatcher {
197
213
  if (key !== null)
198
214
  target.pathname = key;
199
215
  }
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
+ };
317
+ }
200
318
  async prepareEditBatches(statements, context) {
201
319
  const { workspaceId, workerId, loopId, turnId, origin } = context;
202
320
  const schemeCtx = this.#buildSchemeCtx({ workspaceId, workerId, loopId, turnId, origin });
203
321
  const groups = new Map();
204
322
  for (const statement of statements) {
323
+ assertClassifyingSignal(statement); // {§log-tag-signal}
205
324
  const target = this.#extractTarget(statement.target);
206
325
  await this.#canonColumns(target, workspaceId);
207
326
  const key = JSON.stringify([
@@ -213,11 +332,15 @@ class Dispatcher {
213
332
  ]);
214
333
  const group = groups.get(key);
215
334
  if (group === undefined)
216
- groups.set(key, [statement]);
335
+ groups.set(key, {
336
+ identity: renderTarget(target),
337
+ statements: [statement],
338
+ });
217
339
  else
218
- group.push(statement);
340
+ group.statements.push(statement);
219
341
  }
220
- for (const group of groups.values()) {
342
+ for (const preparedGroup of groups.values()) {
343
+ const group = preparedGroup.statements;
221
344
  const first = group[0];
222
345
  const schemeName = schemeNameOf(first.target);
223
346
  let initial;
@@ -248,11 +371,20 @@ class Dispatcher {
248
371
  }
249
372
  else {
250
373
  try {
251
- const addressedScheme = first.target?.kind === "url" ? first.target.scheme : schemeName;
252
- const publishedChannel = first.target?.kind === "url"
253
- ? first.target.fragment ?? manifest.defaultChannel
254
- : manifest.defaultChannel;
255
- initial = Results.assert(await method.call(handler, group, new SchemeCtxImpl(schemeCtx, addressedScheme ?? schemeName, manifest, this.#liveSubscriptions, { publishedChannel })));
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
+ }
256
388
  }
257
389
  catch (err) {
258
390
  if (err instanceof InvalidOperationResultError)
@@ -295,6 +427,7 @@ class Dispatcher {
295
427
  }
296
428
  }
297
429
  async dispatch(context) {
430
+ assertClassifyingSignal(context.statement); // {§log-tag-signal}
298
431
  const result = await this.#dispatchOne(context);
299
432
  if (context.statement.op === "EDIT") {
300
433
  const prepared = this.#preparedEdits.get(context.statement);
@@ -311,6 +444,7 @@ class Dispatcher {
311
444
  const { statement, workspaceId, workerId, loopId, turnId, sequence, origin, onDispatch, } = context;
312
445
  const schemeCtx = this.#buildSchemeCtx({ workspaceId, workerId, loopId, turnId, origin });
313
446
  let result;
447
+ let curationPlan = null;
314
448
  let denial = this.#checkWritable(statement, origin);
315
449
  if (denial === null)
316
450
  denial = await this.#checkFlagsGate(statement, loopId);
@@ -358,7 +492,9 @@ class Dispatcher {
358
492
  result = await this.#handleSendBroadcast(statement, { workspaceId, workerId, loopId, turnId, sequence });
359
493
  }
360
494
  else if (statement.op === "OPEN" || statement.op === "FOLD") {
361
- result = await this.#runLogCuration(statement, schemeCtx);
495
+ const curation = await this.#runLogCuration(statement, schemeCtx);
496
+ result = curation.result;
497
+ curationPlan = curation.plan;
362
498
  }
363
499
  else if (statement.op === "FORK" || statement.op === "WORK") {
364
500
  result = await this.#handleWorkerControl(statement, schemeCtx);
@@ -376,10 +512,8 @@ class Dispatcher {
376
512
  result = this.#handlePlan(statement);
377
513
  }
378
514
  else if (statement.op === "EXEC") {
379
- // EXEC's target slot is `cwd`, not a scheme address.
380
- // Per plurnk.md the op routes unconditionally to the
381
- // exec scheme; the scheme handler reads runtime
382
- // (signal), cwd (target), and command (body).
515
+ // EXEC routes unconditionally to its operation owner. The
516
+ // resolved runtime declaration owns body/target semantics.
383
517
  result = await this.#gatedExec(statement, schemeCtx, loopId, turnId);
384
518
  }
385
519
  else {
@@ -406,19 +540,30 @@ class Dispatcher {
406
540
  // {§join-blocking-collect}
407
541
  if (typeof result.awaitWorker === "string")
408
542
  this.#joinTargets.add(loopId);
409
- const logEntryId = await this.#writeLog({ statement, result, workspaceId, workerId, loopId, turnId, sequence, origin });
543
+ const logEntryId = await this.#writeLog({
544
+ statement,
545
+ result,
546
+ workspaceId,
547
+ workerId,
548
+ loopId,
549
+ turnId,
550
+ sequence,
551
+ origin,
552
+ curationPlan,
553
+ modelCallId: null,
554
+ });
410
555
  // {§search-gate} — register successful searches AFTER #writeLog stamps the runtime
411
556
  // entry's coordinate onto result.attrs.pathname (the gate's dedup serves from it).
412
557
  if (statement.op === "EXEC" && result.status < 400) {
413
558
  const rt = ("signal" in statement && typeof statement.signal === "string" && statement.signal.length > 0) ? statement.signal : "sh";
414
- const cmd = ("body" in statement && typeof statement.body === "string") ? statement.body : "";
559
+ const body = ("body" in statement && typeof statement.body === "string") ? statement.body : "";
415
560
  const attrPath = result.attrs?.pathname;
416
561
  if (typeof attrPath === "string")
417
- this.#searchGate?.registerPending(loopId, turnId, rt, cmd, attrPath);
562
+ this.#searchGate?.registerPending(loopId, turnId, rt, body, attrPath);
418
563
  }
419
564
  onDispatch?.(logEntryId);
420
565
  // Proposal lifecycle (SPEC.md {§engine-rails} + {§methods-proposal-resolve}; {§proposal-202-pauses}). When a
421
- // side-effecting op returns status 202 (a broadcast SEND[202] park is model
566
+ // side-effecting op returns status 202 (a broadcast SEND signal 202 park is model
422
567
  // speech, not a proposal — #isProposal), the entry is written
423
568
  // state='proposed'; dispatch then PAUSES on a per-entry waiter until
424
569
  // resolution arrives via Engine.resolveProposal (from a client-interface resume,
@@ -676,8 +821,8 @@ class Dispatcher {
676
821
  return null;
677
822
  if (statement.op === "SEND" && statement.target === null)
678
823
  return null;
679
- // EXEC's target slot is `cwd`, not a scheme address. The op's
680
- // authority always belongs to the exec scheme regardless of cwd.
824
+ // EXEC's operation authority always belongs to the exec scheme;
825
+ // runtime-specific resource authority is gated separately below.
681
826
  if (statement.op === "EXEC") {
682
827
  return this.#denyIfDisallowed("exec", origin);
683
828
  }
@@ -709,8 +854,8 @@ class Dispatcher {
709
854
  // the prior durable digest and the per-turn cap refuses without execution.
710
855
  async #gatedExec(statement, ctx, loopId, turnId) {
711
856
  const runtime = ("signal" in statement && typeof statement.signal === "string" && statement.signal.length > 0) ? statement.signal : "sh";
712
- const command = ("body" in statement && typeof statement.body === "string") ? statement.body : "";
713
- const verdict = this.#searchGate?.check(loopId, turnId, runtime, command) ?? { verdict: "pass" };
857
+ const body = ("body" in statement && typeof statement.body === "string") ? statement.body : "";
858
+ const verdict = this.#searchGate?.check(loopId, turnId, runtime, body) ?? { verdict: "pass" };
714
859
  if (verdict.verdict === "capped") {
715
860
  return _a.#failure("search-limit-reached", 429, `This turn already used its ${verdict.cap} permitted searches.`, {}, {
716
861
  searchLimit: verdict.cap,
@@ -817,12 +962,17 @@ class Dispatcher {
817
962
  if (statement.op === "COPY" || statement.op === "MOVE") {
818
963
  return check(statement.target) ?? check(statement.body?.target ?? null);
819
964
  }
820
- // {§exec-target-routing} — the operation owner and a non-file source
821
- // are independent authorities; local/file targets stay executor-local.
965
+ // {§exec-target-routing} — only a runtime-declared resource target adds
966
+ // source authority. Literal identifiers and path targets stay executor-local.
822
967
  if (statement.op === "EXEC") {
823
968
  const operationDenial = checkScheme("exec");
824
969
  if (operationDenial !== null)
825
970
  return operationDenial;
971
+ const requested = typeof statement.signal === "string" ? statement.signal : "";
972
+ const runtime = requested === "" ? "sh" : requested;
973
+ const targetKind = this.#executors()?.entry(runtime)?.invocation.target?.kind;
974
+ if (targetKind !== "resource")
975
+ return null;
826
976
  const sourceScheme = schemeNameOf(statement.target);
827
977
  return sourceScheme === null || sourceScheme === "file" ? null : checkScheme(sourceScheme);
828
978
  }
@@ -935,11 +1085,19 @@ class Dispatcher {
935
1085
  retryable: false,
936
1086
  });
937
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;
938
1094
  return this.#moveOrchestration({
939
1095
  statement,
940
1096
  source: {
941
1097
  target: statement.target,
942
- lineMarker: statement.lineMarker,
1098
+ // Canonicalize only the execution selection. #writeLog retains
1099
+ // the authored marker as operation evidence. {§move-canonical-whole-source}
1100
+ lineMarker: sourceLineMarker,
943
1101
  },
944
1102
  destination: statement.body,
945
1103
  ctx,
@@ -1014,35 +1172,48 @@ class Dispatcher {
1014
1172
  // carries the delete target to review (#isProposal fires on 202). Plurnk-internal deletes execute inline.
1015
1173
  return this.#deleteEntry(schemeName, entryPathnameOf(path), ctx);
1016
1174
  }
1017
- // {§model-entry} — mirror a model emission back as an actionless log row, so
1018
- // the model can inspect and curate its own prior behavior. Ordinary admitted
1019
- // emissions are folded; the bounded invalid-emission lifeline is the one
1020
- // born-open rejected item under {§invalid-emission-attempts} and labels
1021
- // that admission fact structurally.
1022
- // Born FOLDED by default (budget-neutral until OPENed); the turn-0 exemplar passes folded:false
1023
- // (born open — the one worked example the model orients on, thinning the grammar). text/vnd.plurnk.
1024
- async writeModelEntry({ verbatim, workerId, loopId, turnId, sequence, folded, origin = "model", admission = "accepted", reasoningItems }) {
1175
+ async #writeActionlessEntry({ verbatim, workerId, loopId, turnId, sequence, origin, kind, folded, modelCallId = null, attrs = {} }) {
1025
1176
  const row = await this.#db.engine_insert_log_entry.get({
1026
1177
  worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence,
1027
- origin, source: null, op: null, suffix: "", signal: null,
1178
+ origin, source: null, model_call_id: modelCallId,
1179
+ op: null, suffix: "", signal: null,
1028
1180
  scheme: null, username: null, password: null, hostname: null, port: null,
1029
1181
  pathname: null, query: null, fragment: null, lineMarker: null,
1030
1182
  tx: "", mimetype_tx: "text/vnd.plurnk",
1031
1183
  rx: JSON.stringify({ content: verbatim, mimetype: "text/vnd.plurnk" }),
1032
1184
  mimetype_rx: "application/json",
1033
1185
  status_rx: 200, tokens: this.#tokenize(verbatim), state: "resolved", outcome: null,
1034
- attrs: JSON.stringify({
1035
- kind: "model_emission",
1036
- ...(admission === "rejected" ? { admission } : {}),
1037
- ...(reasoningItems !== undefined && reasoningItems.length > 0 ? { reasoning: reasoningItems } : {}),
1038
- }),
1186
+ attrs: JSON.stringify({ ...attrs, kind }),
1039
1187
  });
1040
1188
  if (row === undefined)
1041
- throw new Error("Dispatcher.writeModelEntry: insert returned no row");
1189
+ throw new Error("Dispatcher.#writeActionlessEntry: insert returned no row");
1042
1190
  if (folded)
1043
1191
  await this.#db.engine_fold_log_entry.run({ id: row.id });
1044
1192
  return row.id;
1045
1193
  }
1194
+ // {§worker-initialization-entry} — the kernel-authored, born-open worked
1195
+ // turn that initializes a worker's log without impersonating model output.
1196
+ async writeInitializationEntry({ verbatim, workerId, loopId, turnId, sequence }) {
1197
+ return this.#writeActionlessEntry({
1198
+ verbatim, workerId, loopId, turnId, sequence,
1199
+ origin: "plurnk", kind: "initialization", folded: false,
1200
+ });
1201
+ }
1202
+ // {§model-entry} — mirror a model emission back as an actionless log row, so
1203
+ // the model can inspect and curate its own prior behavior. Ordinary admitted
1204
+ // emissions are folded; the bounded invalid-emission lifeline is the one
1205
+ // born-open rejected item under {§invalid-emission-attempts} and labels
1206
+ // that admission fact structurally.
1207
+ async writeModelEntry({ verbatim, workerId, loopId, turnId, sequence, folded, modelCallId, admission = "accepted", reasoningItems }) {
1208
+ return this.#writeActionlessEntry({
1209
+ verbatim, workerId, loopId, turnId, sequence,
1210
+ origin: "model", kind: "model_emission", folded, modelCallId,
1211
+ attrs: {
1212
+ ...(admission === "rejected" ? { admission } : {}),
1213
+ ...(reasoningItems !== undefined && reasoningItems.length > 0 ? { reasoning: reasoningItems } : {}),
1214
+ },
1215
+ });
1216
+ }
1046
1217
  // PLAN — the model's intended-goals op. An ordinary op: dispatched like any
1047
1218
  // other, logged, and broadcast to the client as a log entry — but a pure no-op for
1048
1219
  // state (PLAN ∉ MUTATING_OPS); its body serializes into the log row's tx, no effect.
@@ -1051,6 +1222,20 @@ class Dispatcher {
1051
1222
  throw new Error("unreachable");
1052
1223
  return { status: 200 };
1053
1224
  }
1225
+ // {§bare-inference} Provider work is prepared concurrently by Engine; the
1226
+ // dispatcher owns only the ordinary operation-result commit and notification.
1227
+ async recordBareResult(context, result, modelCallId) {
1228
+ assertClassifyingSignal(context.statement);
1229
+ Results.assert(result);
1230
+ const logEntryId = await this.#writeLog({
1231
+ ...context,
1232
+ result,
1233
+ curationPlan: null,
1234
+ modelCallId,
1235
+ });
1236
+ context.onDispatch?.(logEntryId);
1237
+ return result;
1238
+ }
1054
1239
  static #isDispatchResult(value) {
1055
1240
  return "status" in value;
1056
1241
  }
@@ -1116,7 +1301,57 @@ class Dispatcher {
1116
1301
  manifest,
1117
1302
  };
1118
1303
  }
1119
- async #selectSource(selection, ctx) {
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) {
1120
1355
  const handler = this.#schemes.get(selection.scheme);
1121
1356
  if (handler === undefined) {
1122
1357
  throw new InvalidOperationResultError(`Resolved COPY/MOVE source scheme '${selection.scheme}' is no longer registered.`);
@@ -1155,6 +1390,9 @@ class Dispatcher {
1155
1390
  retryable: false,
1156
1391
  });
1157
1392
  }
1393
+ const resolvedMarker = this.#resolveResourceLineMarker(selection, selected.content, operation);
1394
+ if ("result" in resolvedMarker)
1395
+ return resolvedMarker.result;
1158
1396
  let content = selected.content;
1159
1397
  let scopeNormalizations;
1160
1398
  if (await MimetypeBinary.isBinaryMimetype(selected.mimetype, ctx.mimetypes)) {
@@ -1165,8 +1403,8 @@ class Dispatcher {
1165
1403
  retryable: false,
1166
1404
  });
1167
1405
  }
1168
- if (selection.lineMarker !== null) {
1169
- const sliced = LineMarkerOps.sliceLinesRaw(content, selection.lineMarker);
1406
+ if (resolvedMarker.selection.lineMarker !== null) {
1407
+ const sliced = LineMarkerOps.sliceLinesRaw(content, resolvedMarker.selection.lineMarker);
1170
1408
  if (sliced.status !== 200)
1171
1409
  return Results.assert(sliced);
1172
1410
  content = sliced.text ?? "";
@@ -1176,10 +1414,12 @@ class Dispatcher {
1176
1414
  return Results.assert(selected.producerResult);
1177
1415
  }
1178
1416
  return {
1179
- ...selection,
1417
+ ...resolvedMarker.selection,
1180
1418
  storageAddress,
1181
1419
  content,
1420
+ completeContent: selected.content,
1182
1421
  mimetype: selected.mimetype,
1422
+ lineAnchorPrecondition: resolvedMarker.precondition,
1183
1423
  ...(scopeNormalizations === undefined ? {} : { scopeNormalizations }),
1184
1424
  };
1185
1425
  }
@@ -1194,6 +1434,18 @@ class Dispatcher {
1194
1434
  ],
1195
1435
  });
1196
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
+ }
1197
1449
  #resourceAddress(selection) {
1198
1450
  const address = renderTarget({
1199
1451
  scheme: selection.scheme === "file" ? null : selection.scheme,
@@ -1413,7 +1665,7 @@ class Dispatcher {
1413
1665
  },
1414
1666
  };
1415
1667
  }
1416
- async #invokeEditBatch(selection, edits, ctx) {
1668
+ async #invokeEditBatch(selection, edits, ctx, precondition = null) {
1417
1669
  const handler = this.#schemes.get(selection.scheme);
1418
1670
  if (typeof handler?.editBatch !== "function") {
1419
1671
  return _a.#failure("operation-not-implemented", 501, `Scheme '${selection.scheme}' does not implement EDIT batches.`, {}, {
@@ -1422,10 +1674,10 @@ class Dispatcher {
1422
1674
  retryable: false,
1423
1675
  });
1424
1676
  }
1425
- const statements = edits.map(({ marker, body, tags, position }) => ({
1677
+ const statements = edits.map(({ marker, body, position }) => ({
1426
1678
  op: "EDIT",
1427
1679
  suffix: "",
1428
- signal: tags,
1680
+ signal: null,
1429
1681
  target: selection.target,
1430
1682
  lineMarker: marker,
1431
1683
  body,
@@ -1435,7 +1687,10 @@ class Dispatcher {
1435
1687
  ? selection.target.scheme
1436
1688
  : selection.scheme;
1437
1689
  try {
1438
- const result = Results.assert(await handler.editBatch(statements, new SchemeCtxImpl(ctx, addressedScheme, selection.manifest, this.#liveSubscriptions)));
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
+ })));
1439
1694
  return this.#withProposalRoute(result, selection);
1440
1695
  }
1441
1696
  catch (err) {
@@ -1471,7 +1726,6 @@ class Dispatcher {
1471
1726
  retryable: false,
1472
1727
  });
1473
1728
  }
1474
- const tags = Array.isArray(statement.signal) ? statement.signal : [];
1475
1729
  const destinationEffect = this.#pendingEffect(destination, destinationChannel === undefined ? "create" : "update");
1476
1730
  if (destination.lineMarker !== null) {
1477
1731
  if (destinationChannel === undefined) {
@@ -1487,13 +1741,15 @@ class Dispatcher {
1487
1741
  retryable: false,
1488
1742
  });
1489
1743
  }
1490
- const edited = await this.#invokeEditBatch(destination, [{
1491
- marker: destination.lineMarker,
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,
1492
1749
  body: source.content,
1493
- tags,
1494
1750
  position: statement.position,
1495
- }], ctx);
1496
- return this.#finalizeEffects(edited, destination, [destinationEffect]);
1751
+ }], ctx, resolvedMarker.precondition);
1752
+ return this.#finalizeEffects(edited, resolvedMarker.selection, [destinationEffect]);
1497
1753
  }
1498
1754
  if (destinationChannel !== undefined
1499
1755
  && destinationChannel.content !== source.content) {
@@ -1502,10 +1758,7 @@ class Dispatcher {
1502
1758
  retryable: false,
1503
1759
  });
1504
1760
  }
1505
- const priorTags = existing?.tags ?? [];
1506
- const mergedTags = [...new Set([...priorTags, ...tags])];
1507
- const addsTags = mergedTags.length !== priorTags.length;
1508
- if (destinationChannel !== undefined && !addsTags)
1761
+ if (destinationChannel !== undefined)
1509
1762
  return { status: 304 };
1510
1763
  const channels = {
1511
1764
  ...(existing?.channels ?? {}),
@@ -1514,15 +1767,18 @@ class Dispatcher {
1514
1767
  mimetype: source.mimetype,
1515
1768
  },
1516
1769
  };
1517
- const written = await this.#writeEntry(destination.scheme, destination.pathname, { channels, tags: mergedTags }, ctx);
1770
+ const written = await this.#writeEntry(destination.scheme, destination.pathname, { channels }, ctx);
1518
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;
1519
1775
  const materialized = source.lineMarker === null
1520
1776
  || (exactWritten.status !== 200 && exactWritten.status !== 201 && exactWritten.status !== 202)
1521
1777
  ? exactWritten
1522
- : _a.#withEditMaterialization(exactWritten, editReceipt(destinationChannel?.content ?? "", source.content, [{
1778
+ : _a.#withEditMaterialization(exactWritten, editReceipt("", source.content, [{
1523
1779
  marker: { marks: [1, -1] },
1524
1780
  body: source.content,
1525
- }]));
1781
+ }], parseIssues));
1526
1782
  return this.#finalizeEffects(materialized, destination, [destinationEffect]);
1527
1783
  }
1528
1784
  async #copyOrchestration({ statement, source, destination, ctx, }) {
@@ -1532,13 +1788,13 @@ class Dispatcher {
1532
1788
  const resolvedDestination = await this.#resolveResourceSelection(destination, ctx);
1533
1789
  if (_a.#isDispatchResult(resolvedDestination))
1534
1790
  return resolvedDestination;
1535
- const selected = await this.#selectSource(resolvedSource, ctx);
1791
+ const selected = await this.#selectSource(resolvedSource, ctx, "COPY");
1536
1792
  if (_a.#isDispatchResult(selected))
1537
1793
  return selected;
1538
1794
  const result = await this.#writeDestination(statement, selected, resolvedDestination, ctx);
1539
1795
  return _a.#prependScopeNormalizations(result, selected.scopeNormalizations);
1540
1796
  }
1541
- #deferredMoveSource(source, destination) {
1797
+ #deferredMoveSource(source, destination, lineAnchorPrecondition) {
1542
1798
  return {
1543
1799
  target: source.target,
1544
1800
  lineMarker: source.lineMarker,
@@ -1546,6 +1802,7 @@ class Dispatcher {
1546
1802
  pathname: source.pathname,
1547
1803
  channel: source.channel,
1548
1804
  destination: this.#resourceAddress(destination),
1805
+ lineAnchorPrecondition,
1549
1806
  };
1550
1807
  }
1551
1808
  #moveFailureAfterDestination(result, destination, destinationEffects) {
@@ -1567,7 +1824,7 @@ class Dispatcher {
1567
1824
  });
1568
1825
  return _a.#withCombinedEffects(failed, destinationEffects);
1569
1826
  }
1570
- async #removeMoveSource(statement, source, ctx) {
1827
+ async #removeMoveSource(statement, source, ctx, lineAnchorPrecondition = null) {
1571
1828
  const effect = this.#pendingEffect(source, source.lineMarker === null ? "delete" : "update");
1572
1829
  if (source.lineMarker === null) {
1573
1830
  const deleted = await this.#deleteChannel(source.scheme, source.pathname, source.channel, ctx);
@@ -1576,9 +1833,8 @@ class Dispatcher {
1576
1833
  const edited = await this.#invokeEditBatch(source, [{
1577
1834
  marker: source.lineMarker,
1578
1835
  body: "",
1579
- tags: null,
1580
1836
  position: statement.position,
1581
- }], ctx);
1837
+ }], ctx, lineAnchorPrecondition);
1582
1838
  return this.#finalizeEffects(edited, source, [effect]);
1583
1839
  }
1584
1840
  async #moveWithinChannel(statement, source, destination, ctx) {
@@ -1595,22 +1851,24 @@ class Dispatcher {
1595
1851
  if (destination.lineMarker === null) {
1596
1852
  return this.#writeDestination(statement, source, destination, ctx);
1597
1853
  }
1598
- const moved = await this.#invokeEditBatch(destination, [
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, [
1599
1859
  {
1600
- marker: destination.lineMarker,
1860
+ marker: resolvedDestination.selection.lineMarker,
1601
1861
  body: source.content,
1602
- tags: Array.isArray(statement.signal) ? statement.signal : [],
1603
1862
  position: statement.position,
1604
1863
  },
1605
1864
  {
1606
1865
  marker: source.lineMarker,
1607
1866
  body: "",
1608
- tags: null,
1609
1867
  position: statement.position,
1610
1868
  },
1611
- ], ctx);
1612
- const effect = this.#pendingEffect(destination, "update");
1613
- return this.#finalizeEffects(moved, destination, [effect, effect]);
1869
+ ], ctx, precondition);
1870
+ const effect = this.#pendingEffect(resolvedDestination.selection, "update");
1871
+ return this.#finalizeEffects(moved, resolvedDestination.selection, [effect, effect]);
1614
1872
  }
1615
1873
  async #moveOrchestration({ statement, source, destination, ctx, }) {
1616
1874
  const resolvedSource = await this.#resolveResourceSelection(source, ctx);
@@ -1619,7 +1877,7 @@ class Dispatcher {
1619
1877
  const resolvedDestination = await this.#resolveResourceSelection(destination, ctx);
1620
1878
  if (_a.#isDispatchResult(resolvedDestination))
1621
1879
  return resolvedDestination;
1622
- const selected = await this.#selectSource(resolvedSource, ctx);
1880
+ const selected = await this.#selectSource(resolvedSource, ctx, "MOVE");
1623
1881
  if (_a.#isDispatchResult(selected))
1624
1882
  return selected;
1625
1883
  if (this.#sameChannel(resolvedSource, resolvedDestination)) {
@@ -1638,11 +1896,11 @@ class Dispatcher {
1638
1896
  ...destinationResult,
1639
1897
  attrs: {
1640
1898
  ...destinationResult.attrs,
1641
- moveSource: this.#deferredMoveSource(resolvedSource, resolvedDestination),
1899
+ moveSource: this.#deferredMoveSource(selected, resolvedDestination, selected.lineAnchorPrecondition),
1642
1900
  },
1643
1901
  };
1644
1902
  }
1645
- const sourceResult = await this.#removeMoveSource(statement, resolvedSource, ctx);
1903
+ const sourceResult = await this.#removeMoveSource(statement, selected, ctx, selected.lineAnchorPrecondition);
1646
1904
  if (sourceResult.status === 202) {
1647
1905
  return {
1648
1906
  ...sourceResult,
@@ -1738,7 +1996,10 @@ class Dispatcher {
1738
1996
  || resolvedSource.channel !== deferred.channel) {
1739
1997
  throw new InvalidOperationResultError("A deferred MOVE source no longer resolves to its recorded identity.");
1740
1998
  }
1741
- const removed = await this.#removeMoveSource(statement, resolvedSource, ctx);
1999
+ const removed = await this.#removeMoveSource(statement, {
2000
+ ...resolvedSource,
2001
+ lineMarker: resolvedSource.lineMarker,
2002
+ }, ctx, deferred.lineAnchorPrecondition);
1742
2003
  if (removed.status >= 400) {
1743
2004
  return {
1744
2005
  resolution: settlement.resolution,
@@ -1776,7 +2037,7 @@ class Dispatcher {
1776
2037
  // {§send-premature-terminate} — the unified PENDING SET, judged at the terminal's OWN dispatch
1777
2038
  // (post-batch: the emission's earlier ops already executed, so a same-turn KILL+[200] repairs in
1778
2039
  // ONE turn, and a same-turn WORK+[200] is caught — the spawn is live by the time the SEND lands).
1779
- // pending = open streams ∪ live children ∪ THIS turn's retrievals (READ/FIND/OPEN, results unseen
2040
+ // pending = open streams ∪ live children ∪ THIS turn's retrievals (READ/FIND/OPEN/BARE, results unseen
1780
2041
  // until next packet). Failed operations are the separate next-packet leg shared by explicit
1781
2042
  // completion and empty-join completion. Nothing pending may be silently discarded; 499 discards
1782
2043
  // BY STATED INTENT and is never gated.
@@ -1789,30 +2050,32 @@ class Dispatcher {
1789
2050
  const liveChild = await this.#db.engine_worker_has_live_child.get({ worker_id: workerId });
1790
2051
  if (liveChild !== undefined)
1791
2052
  pending.push("surviving workers");
1792
- const observations = await this.#pendingObservations(workerId, turnId);
1793
- if (observations.retrievals)
2053
+ const boundaries = await this.#nextPacketBoundaries(workerId, turnId);
2054
+ if (boundaries.retrievals)
1794
2055
  pending.push("this turn's retrieval results (they land in the NEXT packet's Log)");
1795
- if (observations.streamTerminations) {
2056
+ if (boundaries.streamTerminations) {
1796
2057
  pending.push("completed stream results that land in the NEXT packet's Log");
1797
2058
  }
1798
- if (observations.childTerminations)
2059
+ if (boundaries.childTerminations)
1799
2060
  pending.push("worker results that arrived during this turn (they land NEXT turn)");
1800
2061
  return pending;
1801
2062
  }
1802
- // Results cross an observation boundary only when they have appeared in a
1803
- // packet. Completion alone is not delivery. Keep the three next-packet
1804
- // producers in one classifier so SEND[200]'s discard gate and SEND[202]'s
1805
- // empty-join decision cannot disagree about what remains unseen.
1806
- async #pendingObservations(workerId, turnId) {
1807
- const [retrievals, streamTermination, childTermination] = await Promise.all([
1808
- this.#db.engine_turn_retrievals.all({ turn_id: turnId }),
2063
+ // Results cross an observation boundary only when they have appeared in a packet;
2064
+ // successful FOLD effects likewise become useful through the curated next packet.
2065
+ // Keep every next-packet boundary in one classifier while letting the callers apply
2066
+ // their distinct contracts: retrievals block explicit completion, whereas FOLD only
2067
+ // prevents an empty wait from being inferred as completion.
2068
+ async #nextPacketBoundaries(workerId, turnId) {
2069
+ const [turnBoundaries, streamTermination, childTermination] = await Promise.all([
2070
+ this.#db.engine_turn_packet_boundaries.all({ turn_id: turnId }),
1809
2071
  this.#db.engine_worker_has_undelivered_stream_term
1810
2072
  .get({ worker_id: workerId }),
1811
2073
  this.#db.engine_worker_has_undelivered_child_term
1812
2074
  .get({ worker_id: workerId }),
1813
2075
  ]);
1814
2076
  return {
1815
- retrievals: retrievals.length > 0,
2077
+ retrievals: turnBoundaries.some(({ op }) => op !== "FOLD"),
2078
+ folds: turnBoundaries.some(({ op }) => op === "FOLD"),
1816
2079
  streamTerminations: streamTermination !== undefined,
1817
2080
  childTerminations: childTermination !== undefined,
1818
2081
  };
@@ -1854,14 +2117,14 @@ class Dispatcher {
1854
2117
  return _a.#failure("send-status-required", 400, "SEND requires a numeric status.", {}, { retryable: false });
1855
2118
  }
1856
2119
  const raw = statement.body === null ? "" : statement.body.raw;
1857
- // The park rides SEND[202] only ({§park-202-only}). A scoped SEND[102] is neither
2120
+ // The park rides SEND signal 202 only ({§park-202-only}). A scoped signal 102 is neither
1858
2121
  // a wait nor a meaningful continuation, so reject it instead of preserving the
1859
2122
  // retired dual spelling.
1860
2123
  if (status === 102 && statement.lineMarker !== null) {
1861
- return _a.#failure("send-scope-invalid", 400, "SEND[102] does not accept a scope.", {}, {
2124
+ return _a.#failure("send-scope-invalid", 400, "`## SEND0 [102]` does not accept a scope.", {}, {
1862
2125
  requestedStatus: 102,
1863
2126
  scope: statement.lineMarker,
1864
- recovery: "Use SEND[202] with a scope to wait, or remove the scope to continue.",
2127
+ recovery: "Use `## SEND0 [202] <scope>` to wait, or remove the scope to continue.",
1865
2128
  retryable: false,
1866
2129
  });
1867
2130
  }
@@ -1875,7 +2138,7 @@ class Dispatcher {
1875
2138
  this.#parkDeadlines.set(loopId, -1); // indefinite: the bounded child's terminal is the wake edge
1876
2139
  return { status: 102, attrs: { parked: -1, join: true } };
1877
2140
  }
1878
- // {§wait-obligation-matrix} — SEND[202] is the obligation-checked join. A live
2141
+ // {§wait-obligation-matrix} — SEND signal 202 is the obligation-checked join. A live
1879
2142
  // obligation (a spawned child or open stream, J) BLOCKS the loop until it concludes and
1880
2143
  // reawakens it ({§worker-lifecycle-child-wake}); a wait on nothing (∅) is already satisfied and
1881
2144
  // resolves like 200, so <-1>+∅ self-resolves rather than hang the agent; a pending own
@@ -1894,8 +2157,8 @@ class Dispatcher {
1894
2157
  // all complete-but-unobserved. Their wake edge may already have
1895
2158
  // fired, so do not park; continue directly to the packet that
1896
2159
  // materializes them.
1897
- const observations = await this.#pendingObservations(workerId, turnId);
1898
- if (observations.retrievals || observations.streamTerminations || observations.childTerminations) {
2160
+ const boundaries = await this.#nextPacketBoundaries(workerId, turnId);
2161
+ if (boundaries.retrievals || boundaries.folds || boundaries.streamTerminations || boundaries.childTerminations) {
1899
2162
  return { status: 102 };
1900
2163
  }
1901
2164
  const failCount = await this.#unobservedFailureCount(turnId);
@@ -1950,7 +2213,7 @@ class Dispatcher {
1950
2213
  return _a.#failure("retrieval-results-unobserved", 409, "Last turn both performed retrieval operations and attempted to terminate. Retrieval operations force an additional turn so their results can be reviewed.", {}, {
1951
2214
  pending: [...pending],
1952
2215
  stage: "completion",
1953
- recovery: "Review the results, then use only PLAN and SEND[200] to conclude.",
2216
+ recovery: "Review the results, then use only `# PLAN0` and `## SEND0 [200]` to conclude.",
1954
2217
  retryable: false,
1955
2218
  });
1956
2219
  }
@@ -1977,7 +2240,7 @@ class Dispatcher {
1977
2240
  turn_id: turnId,
1978
2241
  });
1979
2242
  if (seqs === undefined) {
1980
- throw new Error(`SEND[499]: no coordinate for loop=${loopId} turn=${turnId}`);
2243
+ throw new Error(`SEND signal 499: no coordinate for loop=${loopId} turn=${turnId}`);
1981
2244
  }
1982
2245
  Results.attachInstance(failure, `log:///${seqs.loop_seq}/${seqs.turn_seq}/${ctx.sequence}/SEND`);
1983
2246
  const finished = await this.#lifecycle.finish(loopId, failure);
@@ -2070,6 +2333,7 @@ class Dispatcher {
2070
2333
  statement,
2071
2334
  manifest,
2072
2335
  target,
2336
+ identity: target,
2073
2337
  representation: resolved.representation,
2074
2338
  mimetypes: ctx.mimetypes,
2075
2339
  }));
@@ -2171,11 +2435,14 @@ class Dispatcher {
2171
2435
  async #runLogCuration(statement, ctx) {
2172
2436
  const addressedScheme = schemeNameOf(statement.target);
2173
2437
  if (addressedScheme !== null && addressedScheme !== "log") {
2174
- return _a.#failure("operation-not-implemented", 501, `Scheme '${addressedScheme}' does not implement ${statement.op}.`, {}, {
2175
- scheme: addressedScheme,
2176
- operation: statement.op,
2177
- retryable: false,
2178
- });
2438
+ return {
2439
+ result: _a.#failure("operation-not-implemented", 501, `Scheme '${addressedScheme}' does not implement ${statement.op}.`, {}, {
2440
+ scheme: addressedScheme,
2441
+ operation: statement.op,
2442
+ retryable: false,
2443
+ }),
2444
+ plan: null,
2445
+ };
2179
2446
  }
2180
2447
  const handler = this.#schemes.get("log");
2181
2448
  const manifest = this.#schemes.manifestFor("log");
@@ -2183,21 +2450,20 @@ class Dispatcher {
2183
2450
  throw new Error("the core log curation owner is not registered");
2184
2451
  }
2185
2452
  const schemeCtx = new SchemeCtxImpl(ctx, "log", manifest, this.#liveSubscriptions);
2186
- return statement.op === "OPEN"
2187
- ? Results.assert(await handler.open(statement, schemeCtx))
2188
- : Results.assert(await handler.fold(statement, schemeCtx));
2453
+ const outcome = await handler.curate(statement, schemeCtx);
2454
+ return { result: Results.assert(outcome.result), plan: outcome.plan };
2189
2455
  }
2190
2456
  // {§proposal}/{§send} — status 202 is a proposal except for broadcast
2191
- // SEND[202], which parks the loop. The operation disambiguates the status.
2457
+ // SEND signal 202, which parks the loop. The operation disambiguates the status.
2192
2458
  static #isProposal(statement, result) {
2193
- // {§send-300-choices} — SEND[300] is the proposal exception.
2459
+ // {§send-300-choices} — SEND signal 300 is the proposal exception.
2194
2460
  if (result.status !== 202)
2195
2461
  return false;
2196
2462
  if (statement.op === "SEND" && statement.signal === 300)
2197
2463
  return true;
2198
2464
  return !(statement.op === "SEND" && statement.target === null);
2199
2465
  }
2200
- async #writeLog({ statement, result, workspaceId, workerId, loopId, turnId, sequence, origin, }) {
2466
+ async #writeLog({ statement, result, workspaceId, workerId, loopId, turnId, sequence, origin, curationPlan, modelCallId, }) {
2201
2467
  const durableStatement = DurableStatement.project(statement);
2202
2468
  const target = this.#extractTarget(durableStatement.target);
2203
2469
  await this.#canonColumns(target, workspaceId); // {§fs-answer-in-canon}
@@ -2207,12 +2473,23 @@ class Dispatcher {
2207
2473
  // A proposal (status 202 from a side-effecting op) is written to the log in
2208
2474
  // state='proposed' until the proposal lifecycle resolves it; attrs holds the
2209
2475
  // scheme-supplied payload (file diff, exec command, etc.) the client renders
2210
- // for review and the scheme consumes on accept. A broadcast SEND[202] is a
2476
+ // for review and the scheme consumes on accept. A broadcast SEND signal 202 is a
2211
2477
  // parked-terminal, not a proposal (#isProposal) → state='resolved'.
2212
2478
  const isProposed = _a.#isProposal(statement, result);
2213
2479
  let attrsObj = (result.attrs !== undefined && result.attrs !== null)
2214
2480
  ? { ...result.attrs }
2215
2481
  : {};
2482
+ if (curationPlan !== null) {
2483
+ if (Object.hasOwn(attrsObj, "__plurnk_curation")) {
2484
+ throw new Error("Dispatcher.#writeLog: result attrs collide with private log curation state");
2485
+ }
2486
+ attrsObj.__plurnk_curation = {
2487
+ ids: curationPlan.targetLogEntryIds,
2488
+ expanded: curationPlan.expanded,
2489
+ add: curationPlan.add,
2490
+ remove: curationPlan.remove,
2491
+ };
2492
+ }
2216
2493
  const seqs = statement.op === "EXEC" || result.problem !== undefined
2217
2494
  ? await this.#db.engine_loop_turn_seqs.get({
2218
2495
  loop_id: loopId,
@@ -2263,6 +2540,7 @@ class Dispatcher {
2263
2540
  sequence: sequence,
2264
2541
  origin,
2265
2542
  source: null, // dispatch entries are self-authored; {§env-delta} deltas set this
2543
+ model_call_id: modelCallId,
2266
2544
  op: durableStatement.op,
2267
2545
  suffix: durableStatement.suffix,
2268
2546
  signal: this.#signalToJson(durableStatement.signal),