@plurnk/plurnk-service 1.10.0 → 1.11.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 (288) hide show
  1. package/.env.defaults +41 -17
  2. package/INSTALL.md +1 -1
  3. package/README.md +1 -1
  4. package/SPEC.md +487 -233
  5. package/digest-sql/curation/curation.sql +1 -0
  6. package/dist/Paths.d.ts +3 -3
  7. package/dist/Paths.d.ts.map +1 -1
  8. package/dist/Paths.js +9 -9
  9. package/dist/Paths.js.map +1 -1
  10. package/dist/build-info.json +1 -1
  11. package/dist/content/edit-collision.js +3 -3
  12. package/dist/content/edit-collision.js.map +1 -1
  13. package/dist/content/edit-receipt.d.ts +6 -5
  14. package/dist/content/edit-receipt.d.ts.map +1 -1
  15. package/dist/content/edit-receipt.js +13 -2
  16. package/dist/content/edit-receipt.js.map +1 -1
  17. package/dist/content/index.d.ts +2 -2
  18. package/dist/content/index.d.ts.map +1 -1
  19. package/dist/content/index.js +1 -1
  20. package/dist/content/index.js.map +1 -1
  21. package/dist/content/line-anchors.d.ts +2 -0
  22. package/dist/content/line-anchors.d.ts.map +1 -1
  23. package/dist/content/line-anchors.js +2 -0
  24. package/dist/content/line-anchors.js.map +1 -1
  25. package/dist/content/matcher.d.ts.map +1 -1
  26. package/dist/content/matcher.js +3 -2
  27. package/dist/content/matcher.js.map +1 -1
  28. package/dist/content/read-projector.js +3 -3
  29. package/dist/content/read-projector.js.map +1 -1
  30. package/dist/core/BudgetReadout.d.ts +6 -1
  31. package/dist/core/BudgetReadout.d.ts.map +1 -1
  32. package/dist/core/BudgetReadout.js +38 -2
  33. package/dist/core/BudgetReadout.js.map +1 -1
  34. package/dist/core/CapabilityPolicies.d.ts +15 -0
  35. package/dist/core/CapabilityPolicies.d.ts.map +1 -0
  36. package/dist/core/CapabilityPolicies.js +60 -0
  37. package/dist/core/CapabilityPolicies.js.map +1 -0
  38. package/dist/core/CapabilityResolver.d.ts +24 -0
  39. package/dist/core/CapabilityResolver.d.ts.map +1 -0
  40. package/dist/core/CapabilityResolver.js +180 -0
  41. package/dist/core/CapabilityResolver.js.map +1 -0
  42. package/dist/core/ChannelWrite.d.ts +4 -3
  43. package/dist/core/ChannelWrite.d.ts.map +1 -1
  44. package/dist/core/CoreSchemeServices.d.ts +1 -1
  45. package/dist/core/CoreSchemeServices.d.ts.map +1 -1
  46. package/dist/core/CoreSchemeServices.js +1 -1
  47. package/dist/core/CoreSchemeServices.js.map +1 -1
  48. package/dist/core/Dispatcher.d.ts +3 -1
  49. package/dist/core/Dispatcher.d.ts.map +1 -1
  50. package/dist/core/Dispatcher.js +148 -160
  51. package/dist/core/Dispatcher.js.map +1 -1
  52. package/dist/core/DurableStatement.d.ts.map +1 -1
  53. package/dist/core/DurableStatement.js +19 -15
  54. package/dist/core/DurableStatement.js.map +1 -1
  55. package/dist/core/Engine.d.ts +6 -4
  56. package/dist/core/Engine.d.ts.map +1 -1
  57. package/dist/core/Engine.js +18 -7
  58. package/dist/core/Engine.js.map +1 -1
  59. package/dist/core/Engine.sql +30 -25
  60. package/dist/core/ExecutorRegistry.d.ts +3 -3
  61. package/dist/core/ExecutorRegistry.d.ts.map +1 -1
  62. package/dist/core/ExecutorRegistry.js +10 -6
  63. package/dist/core/ExecutorRegistry.js.map +1 -1
  64. package/dist/core/GitBranch.d.ts +4 -1
  65. package/dist/core/GitBranch.d.ts.map +1 -1
  66. package/dist/core/GitBranch.js +12 -2
  67. package/dist/core/GitBranch.js.map +1 -1
  68. package/dist/core/LogEntryProjection.d.ts +1 -0
  69. package/dist/core/LogEntryProjection.d.ts.map +1 -1
  70. package/dist/core/LogEntryProjection.js +12 -4
  71. package/dist/core/LogEntryProjection.js.map +1 -1
  72. package/dist/core/LoopPolicyReader.d.ts +7 -0
  73. package/dist/core/LoopPolicyReader.d.ts.map +1 -0
  74. package/dist/core/LoopPolicyReader.js +33 -0
  75. package/dist/core/LoopPolicyReader.js.map +1 -0
  76. package/dist/core/ModelCall.d.ts +1 -0
  77. package/dist/core/ModelCall.d.ts.map +1 -1
  78. package/dist/core/ModelCall.js +3 -0
  79. package/dist/core/ModelCall.js.map +1 -1
  80. package/dist/core/NoticeChannel.d.ts +2 -2
  81. package/dist/core/NoticeChannel.d.ts.map +1 -1
  82. package/dist/core/NoticeChannel.js +18 -9
  83. package/dist/core/NoticeChannel.js.map +1 -1
  84. package/dist/core/OperatorConfig.d.ts.map +1 -1
  85. package/dist/core/OperatorConfig.js +4 -0
  86. package/dist/core/OperatorConfig.js.map +1 -1
  87. package/dist/core/OverflowTurn.d.ts.map +1 -1
  88. package/dist/core/OverflowTurn.js +4 -3
  89. package/dist/core/OverflowTurn.js.map +1 -1
  90. package/dist/core/Owner.js +5 -5
  91. package/dist/core/Owner.js.map +1 -1
  92. package/dist/core/PacketBuilder.d.ts +2 -2
  93. package/dist/core/PacketBuilder.d.ts.map +1 -1
  94. package/dist/core/PacketBuilder.js +53 -35
  95. package/dist/core/PacketBuilder.js.map +1 -1
  96. package/dist/core/ProblemLog.d.ts.map +1 -1
  97. package/dist/core/ProblemLog.js +1 -0
  98. package/dist/core/ProblemLog.js.map +1 -1
  99. package/dist/core/ProposalLifecycle.d.ts.map +1 -1
  100. package/dist/core/ProposalLifecycle.js +16 -13
  101. package/dist/core/ProposalLifecycle.js.map +1 -1
  102. package/dist/core/ReasoningEvent.d.ts +1 -0
  103. package/dist/core/ReasoningEvent.d.ts.map +1 -1
  104. package/dist/core/ResourceMutations.d.ts +5 -4
  105. package/dist/core/ResourceMutations.d.ts.map +1 -1
  106. package/dist/core/ResourceMutations.js +71 -48
  107. package/dist/core/ResourceMutations.js.map +1 -1
  108. package/dist/core/SchemeRegistry.d.ts +6 -3
  109. package/dist/core/SchemeRegistry.d.ts.map +1 -1
  110. package/dist/core/SchemeRegistry.js +14 -17
  111. package/dist/core/SchemeRegistry.js.map +1 -1
  112. package/dist/core/ServiceTeardown.d.ts +1 -1
  113. package/dist/core/ServiceTeardown.d.ts.map +1 -1
  114. package/dist/core/ServiceTeardown.js +4 -8
  115. package/dist/core/ServiceTeardown.js.map +1 -1
  116. package/dist/core/StrikeRail.js +1 -1
  117. package/dist/core/StrikeRail.js.map +1 -1
  118. package/dist/core/ToolResources.d.ts +2 -2
  119. package/dist/core/ToolResources.d.ts.map +1 -1
  120. package/dist/core/ToolResources.js +18 -8
  121. package/dist/core/ToolResources.js.map +1 -1
  122. package/dist/core/TurnOps.d.ts.map +1 -1
  123. package/dist/core/TurnOps.js +22 -8
  124. package/dist/core/TurnOps.js.map +1 -1
  125. package/dist/core/TurnRunner.d.ts +7 -3
  126. package/dist/core/TurnRunner.d.ts.map +1 -1
  127. package/dist/core/TurnRunner.js +315 -71
  128. package/dist/core/TurnRunner.js.map +1 -1
  129. package/dist/core/WorkerControlAddress.d.ts.map +1 -1
  130. package/dist/core/WorkerControlAddress.js +1 -2
  131. package/dist/core/WorkerControlAddress.js.map +1 -1
  132. package/dist/core/WorkerName.d.ts +2 -0
  133. package/dist/core/WorkerName.d.ts.map +1 -1
  134. package/dist/core/WorkerName.js +3 -2
  135. package/dist/core/WorkerName.js.map +1 -1
  136. package/dist/core/WorkerName.sql +2 -1
  137. package/dist/core/caps/DbProjectionCaps.d.ts +2 -1
  138. package/dist/core/caps/DbProjectionCaps.d.ts.map +1 -1
  139. package/dist/core/caps/DbProjectionCaps.js +12 -1
  140. package/dist/core/caps/DbProjectionCaps.js.map +1 -1
  141. package/dist/core/file-materialization.d.ts +22 -0
  142. package/dist/core/file-materialization.d.ts.map +1 -0
  143. package/dist/core/file-materialization.js +77 -0
  144. package/dist/core/file-materialization.js.map +1 -0
  145. package/dist/core/fork.d.ts +2 -1
  146. package/dist/core/fork.d.ts.map +1 -1
  147. package/dist/core/fork.js +13 -4
  148. package/dist/core/fork.js.map +1 -1
  149. package/dist/core/fork.sql +27 -11
  150. package/dist/core/git-membership.d.ts +25 -14
  151. package/dist/core/git-membership.d.ts.map +1 -1
  152. package/dist/core/git-membership.js +260 -164
  153. package/dist/core/git-membership.js.map +1 -1
  154. package/dist/core/git-state.d.ts +2 -0
  155. package/dist/core/git-state.d.ts.map +1 -1
  156. package/dist/core/git-state.js +27 -1
  157. package/dist/core/git-state.js.map +1 -1
  158. package/dist/core/operation-target-groups.d.ts.map +1 -1
  159. package/dist/core/operation-target-groups.js +8 -7
  160. package/dist/core/operation-target-groups.js.map +1 -1
  161. package/dist/core/owner.sql +7 -10
  162. package/dist/core/packet-wire.d.ts +10 -0
  163. package/dist/core/packet-wire.d.ts.map +1 -1
  164. package/dist/core/packet-wire.js +78 -29
  165. package/dist/core/packet-wire.js.map +1 -1
  166. package/dist/core/scheme-types.d.ts +2 -2
  167. package/dist/core/scheme-types.d.ts.map +1 -1
  168. package/dist/core/scheme-types.js +1 -1
  169. package/dist/core/scheme-types.js.map +1 -1
  170. package/dist/core/types.d.ts +3 -2
  171. package/dist/core/types.d.ts.map +1 -1
  172. package/dist/core/types.js +1 -1
  173. package/dist/core/types.js.map +1 -1
  174. package/dist/core/worker-settings.d.ts +4 -3
  175. package/dist/core/worker-settings.d.ts.map +1 -1
  176. package/dist/core/worker-settings.js +40 -19
  177. package/dist/core/worker-settings.js.map +1 -1
  178. package/dist/core/workspace-settings.d.ts +3 -1
  179. package/dist/core/workspace-settings.d.ts.map +1 -1
  180. package/dist/core/workspace-settings.js +6 -2
  181. package/dist/core/workspace-settings.js.map +1 -1
  182. package/dist/digest/Digest.d.ts.map +1 -1
  183. package/dist/digest/Digest.js +93 -26
  184. package/dist/digest/Digest.js.map +1 -1
  185. package/dist/digest/digest.sql +13 -3
  186. package/dist/index.js +1 -1
  187. package/dist/index.js.map +1 -1
  188. package/dist/schemes/Exec.d.ts +3 -5
  189. package/dist/schemes/Exec.d.ts.map +1 -1
  190. package/dist/schemes/Exec.js +98 -94
  191. package/dist/schemes/Exec.js.map +1 -1
  192. package/dist/schemes/ExecOutputScheme.d.ts.map +1 -1
  193. package/dist/schemes/ExecOutputScheme.js +24 -6
  194. package/dist/schemes/ExecOutputScheme.js.map +1 -1
  195. package/dist/schemes/ExecScheduler.d.ts +15 -0
  196. package/dist/schemes/ExecScheduler.d.ts.map +1 -0
  197. package/dist/schemes/ExecScheduler.js +95 -0
  198. package/dist/schemes/ExecScheduler.js.map +1 -0
  199. package/dist/schemes/File.d.ts.map +1 -1
  200. package/dist/schemes/File.js +16 -15
  201. package/dist/schemes/File.js.map +1 -1
  202. package/dist/schemes/Log.d.ts +6 -4
  203. package/dist/schemes/Log.d.ts.map +1 -1
  204. package/dist/schemes/Log.js +131 -38
  205. package/dist/schemes/Log.js.map +1 -1
  206. package/dist/schemes/Log.sql +23 -18
  207. package/dist/schemes/QuestionTool.d.ts +17 -4
  208. package/dist/schemes/QuestionTool.d.ts.map +1 -1
  209. package/dist/schemes/QuestionTool.js +2 -4
  210. package/dist/schemes/QuestionTool.js.map +1 -1
  211. package/dist/schemes/Worker.d.ts.map +1 -1
  212. package/dist/schemes/Worker.js +20 -20
  213. package/dist/schemes/Worker.js.map +1 -1
  214. package/dist/schemes/_entry-chunk.js +1 -1
  215. package/dist/schemes/_entry-chunk.js.map +1 -1
  216. package/dist/schemes/_entry-crud.sql +18 -17
  217. package/dist/schemes/_entry-find.d.ts.map +1 -1
  218. package/dist/schemes/_entry-find.js +7 -9
  219. package/dist/schemes/_entry-find.js.map +1 -1
  220. package/dist/schemes/_entry-graph.js +12 -12
  221. package/dist/schemes/_entry-graph.js.map +1 -1
  222. package/dist/schemes/_entry-graph.sql +5 -5
  223. package/dist/schemes/_entry-ops.d.ts.map +1 -1
  224. package/dist/schemes/_entry-ops.js +3 -3
  225. package/dist/schemes/_entry-ops.js.map +1 -1
  226. package/dist/schemes/_entry-semantic.js +1 -1
  227. package/dist/schemes/_entry-semantic.js.map +1 -1
  228. package/dist/server/BranchBatches.d.ts +3 -3
  229. package/dist/server/BranchBatches.d.ts.map +1 -1
  230. package/dist/server/BranchBatches.js +9 -2
  231. package/dist/server/BranchBatches.js.map +1 -1
  232. package/dist/server/Daemon.d.ts +12 -41
  233. package/dist/server/Daemon.d.ts.map +1 -1
  234. package/dist/server/Daemon.js +59 -93
  235. package/dist/server/Daemon.js.map +1 -1
  236. package/dist/server/DaemonModule.d.ts +10 -1
  237. package/dist/server/DaemonModule.d.ts.map +1 -1
  238. package/dist/server/DrainSupervisor.d.ts +6 -5
  239. package/dist/server/DrainSupervisor.d.ts.map +1 -1
  240. package/dist/server/DrainSupervisor.js +14 -11
  241. package/dist/server/DrainSupervisor.js.map +1 -1
  242. package/dist/server/Functionality.d.ts +2 -2
  243. package/dist/server/Functionality.d.ts.map +1 -1
  244. package/dist/server/Functionality.js +8 -5
  245. package/dist/server/Functionality.js.map +1 -1
  246. package/dist/server/FunctionalityManager.d.ts +13 -1
  247. package/dist/server/FunctionalityManager.d.ts.map +1 -1
  248. package/dist/server/FunctionalityManager.js +82 -17
  249. package/dist/server/FunctionalityManager.js.map +1 -1
  250. package/dist/server/MembersFunctionality.d.ts +56 -0
  251. package/dist/server/MembersFunctionality.d.ts.map +1 -0
  252. package/dist/server/MembersFunctionality.js +350 -0
  253. package/dist/server/MembersFunctionality.js.map +1 -0
  254. package/dist/server/SkillsFunctionality.d.ts +12 -1
  255. package/dist/server/SkillsFunctionality.d.ts.map +1 -1
  256. package/dist/server/SkillsFunctionality.js +6 -1
  257. package/dist/server/SkillsFunctionality.js.map +1 -1
  258. package/dist/server/client-input.d.ts +5 -8
  259. package/dist/server/client-input.d.ts.map +1 -1
  260. package/dist/server/client-input.js +49 -108
  261. package/dist/server/client-input.js.map +1 -1
  262. package/dist/server/dispatch-as-plurnk.d.ts.map +1 -1
  263. package/dist/server/dispatch-as-plurnk.js +3 -0
  264. package/dist/server/dispatch-as-plurnk.js.map +1 -1
  265. package/dist/server/drain.sql +4 -4
  266. package/dist/server/envelope.d.ts +1 -1
  267. package/dist/server/envelope.d.ts.map +1 -1
  268. package/dist/server/envelope.js +11 -10
  269. package/dist/server/envelope.js.map +1 -1
  270. package/dist/server/envelope.sql +1 -1
  271. package/dist/server/loopDocs.d.ts.map +1 -1
  272. package/dist/server/loopDocs.js +12 -5
  273. package/dist/server/loopDocs.js.map +1 -1
  274. package/dist/server/seam-proposal-list.sql +2 -2
  275. package/dist/server/worker-capabilities.sql +9 -0
  276. package/dist/service.d.ts.map +1 -1
  277. package/dist/service.js +16 -21
  278. package/dist/service.js.map +1 -1
  279. package/migrations/001_schema.sql +217 -64
  280. package/package.json +30 -32
  281. package/dist/core/LoopFlagsReader.d.ts +0 -7
  282. package/dist/core/LoopFlagsReader.d.ts.map +0 -1
  283. package/dist/core/LoopFlagsReader.js +0 -33
  284. package/dist/core/LoopFlagsReader.js.map +0 -1
  285. package/dist/core/resolveForLoop.d.ts +0 -5
  286. package/dist/core/resolveForLoop.d.ts.map +0 -1
  287. package/dist/core/resolveForLoop.js +0 -12
  288. package/dist/core/resolveForLoop.js.map +0 -1
@@ -6,7 +6,9 @@ import Fork from "./fork.js";
6
6
  import WorkerCap from "./worker-cap.js";
7
7
  import { PathSyntax, TagSignal } from "@plurnk/plurnk-contracts";
8
8
  import Namespace from "./namespace.js";
9
- import LoopFlagsReader from "./LoopFlagsReader.js";
9
+ import LoopPolicyReader from "./LoopPolicyReader.js";
10
+ import CapabilityResolver from "./CapabilityResolver.js";
11
+ import CapabilityPolicies from "./CapabilityPolicies.js";
10
12
  import ChannelWrite from "./ChannelWrite.js";
11
13
  import { ReadProjector } from "../content/index.js";
12
14
  import SchemeCtxImpl from "./caps/SchemeCtxImpl.js";
@@ -17,12 +19,13 @@ import TerminalResult from "./TerminalResult.js";
17
19
  import Results from "./results.js";
18
20
  import { OperationFailureError } from "./results.js";
19
21
  import EffectPolicy from "../schemes/EffectPolicy.js";
20
- import WorkerSettingsReader from "./worker-settings.js";
21
22
  import { CoreSchemeAdapterBase } from "./CoreSchemeServices.js";
22
23
  import { InvalidOperationResultError, } from "@plurnk/plurnk-schemes";
23
24
  import DurableStatement from "./DurableStatement.js";
24
25
  import ResourceMutations from "./ResourceMutations.js";
25
26
  import LogBody from "./LogBody.js";
27
+ import LogVisibility from "./LogVisibility.js";
28
+ import ErrorDetail from "./ErrorDetail.js";
26
29
  // SPEC {§scheme-surface}: writer must be in target scheme's manifest.writableBy.
27
30
  // OPEN/FOLD/READ/FIND are not gated — they curate the log or read, never mutating an entry.
28
31
  const MUTATING_OPS = new Set(["EDIT", "SEND", "COPY", "MOVE", "EXEC", "KILL", "FORK", "WORK"]);
@@ -38,6 +41,8 @@ const assertClassifyingSignal = (statement) => {
38
41
  if (statement.op === "EXEC")
39
42
  TagSignal.applied(statement.tags ?? null); // {§exec-tag-signal}
40
43
  };
44
+ const primaryTargetOf = (statement) => statement.op === "COPY" || statement.op === "MOVE" ? statement.source.target : statement.target;
45
+ const primaryLineMarkerOf = (statement) => statement.op === "COPY" || statement.op === "MOVE" ? statement.source.lineMarker : statement.lineMarker;
41
46
  // Op dispatch ({§op-methods-op-dispatch}): admission, operation-owner routing,
42
47
  // durable log writing, and proposal lifecycle.
43
48
  class Dispatcher {
@@ -79,6 +84,7 @@ class Dispatcher {
79
84
  #lifecycle;
80
85
  #resourceMutations;
81
86
  #entryAddresses;
87
+ #capabilities;
82
88
  constructor({ db, schemes, mimetypes, weigh, notices, proposals, interactions, executors, loopSignal, settleDerivations, streamEventNotify, wakeWorkerNotify, injectWorker, branchWorker, branchCompletionGate, cancelWorker, cancelDescendants, parkDeadlines, joinTargets, liveSubscriptions, entryAddresses }) {
83
89
  this.#db = db;
84
90
  this.#schemes = schemes;
@@ -101,13 +107,14 @@ class Dispatcher {
101
107
  this.#joinTargets = joinTargets ?? new Set();
102
108
  this.#liveSubscriptions = liveSubscriptions;
103
109
  this.#entryAddresses = entryAddresses;
110
+ this.#capabilities = new CapabilityResolver(db, schemes, executors);
104
111
  this.#lifecycle = new LoopLifecycle(db);
105
112
  this.#resourceMutations = new ResourceMutations({
106
113
  schemes,
107
114
  liveSubscriptions,
108
115
  run: (schemeName, statement, ctx) => this.#run(schemeName, statement, ctx),
109
116
  checkWritable: (statement, origin, workerId) => this.#checkWritable(statement, origin, workerId),
110
- checkFlagsGate: (statement, loopId, workerId) => this.#checkFlagsGate(statement, loopId, workerId),
117
+ checkCapabilities: (statement, workspaceId, loopId, workerId) => this.#checkCapabilities(statement, workspaceId, loopId, workerId),
111
118
  editTargetIdentity: (statement, workspaceId, workerId) => this.#editTargetIdentity(statement, workspaceId, workerId),
112
119
  canonicalFilePath: (pathname, workspaceId) => this.#canonicalFilePath(pathname, workspaceId),
113
120
  prepareDataRepresentation: (args) => this.#prepareDataRepresentation({
@@ -290,14 +297,14 @@ class Dispatcher {
290
297
  return result;
291
298
  }
292
299
  async #dispatchOne(context) {
293
- const { statement, workspaceId, workerId, loopId, turnId, sequence, origin, onDispatch, } = context;
300
+ const { statement, workspaceId, workerId, loopId, turnId, sequence, origin, onDispatch, onSettled, } = context;
294
301
  const schemeCtx = this.#buildSchemeCtx({ workspaceId, workerId, functionalityWorkerId: context.functionalityWorkerId, loopId, turnId, origin });
295
302
  const { functionalityWorkerId } = schemeCtx;
296
303
  let result;
297
304
  let curationPlan = null;
298
305
  let denial = this.#checkWritable(statement, origin, functionalityWorkerId);
299
306
  if (denial === null)
300
- denial = await this.#checkFlagsGate(statement, loopId, functionalityWorkerId);
307
+ denial = await this.#checkCapabilities(statement, workspaceId, loopId, functionalityWorkerId);
301
308
  if (denial !== null) {
302
309
  result = denial;
303
310
  }
@@ -321,7 +328,9 @@ class Dispatcher {
321
328
  origin,
322
329
  });
323
330
  }
324
- else if (statement.op === "OPEN" || statement.op === "FOLD") {
331
+ else if (statement.op === "OPEN"
332
+ || statement.op === "FOLD"
333
+ || (statement.op === "KILL" && schemeNameOf(statement.target) === "log")) {
325
334
  const curation = await this.#runLogCuration(statement, schemeCtx);
326
335
  result = curation.result;
327
336
  curationPlan = curation.plan;
@@ -342,22 +351,9 @@ class Dispatcher {
342
351
  result = this.#handlePlan(statement);
343
352
  }
344
353
  else if (statement.op === "EXEC") {
345
- // {§worker-tool-admission} — the question runtime is admitted
346
- // per worker: a worker whose own rules don't request user input
347
- // gets the explicit not-available outcome, never a parked loop.
348
- if (("signal" in statement && typeof statement.signal === "string")
349
- && !(await WorkerSettingsReader.toolAvailable(this.#db, schemeCtx.functionalityWorkerId, statement.signal))) {
350
- result = _a.#failure("question-tool-unavailable", 404, "The question tool is not available to this worker.", {}, {
351
- stage: "tool-admission",
352
- recovery: "This worker does not request user input; continue from the available evidence.",
353
- retryable: false,
354
- });
355
- }
356
- else {
357
- // EXEC routes unconditionally to its operation owner. The
358
- // resolved runtime declaration owns body/target semantics.
359
- result = await this.#run("exec", statement, schemeCtx);
360
- }
354
+ // EXEC routes unconditionally to its operation owner after
355
+ // the shared capability resolver admits its runtime/tool.
356
+ result = await this.#run("exec", statement, schemeCtx);
361
357
  }
362
358
  else {
363
359
  result = await this.#run(schemeNameOf(statement.target), statement, schemeCtx); // {§op-methods-op-dispatch}
@@ -370,7 +366,7 @@ class Dispatcher {
370
366
  result = err.result;
371
367
  }
372
368
  else {
373
- const scheme = schemeNameOf(statement.target);
369
+ const scheme = schemeNameOf(primaryTargetOf(statement));
374
370
  console.error(`Scheme '${scheme ?? "unknown"}' ${statement.op} threw outside its operation result contract:`, err);
375
371
  result = _a.#failure("scheme-handler-threw", 500, `The '${scheme ?? "unknown"}' scheme did not produce a ${statement.op} result.`, {}, {
376
372
  stage: "scheme-dispatch",
@@ -380,8 +376,8 @@ class Dispatcher {
380
376
  }
381
377
  }
382
378
  }
383
- // {§fold-open-meta-operations} — persist OPEN/FOLD for forensics;
384
- // packet rendering suppresses their successful receipts.
379
+ // Persist log curation for forensics; packet rendering suppresses its
380
+ // successful receipts while the exact state effects remain durable.
385
381
  // A running-worker READ arms this turn's blocking collect.
386
382
  // {§join-blocking-collect}
387
383
  if (typeof result.awaitWorker === "string")
@@ -428,7 +424,9 @@ class Dispatcher {
428
424
  ctx: schemeCtx,
429
425
  ids: { workspaceId, workerId, functionalityWorkerId, loopId, turnId },
430
426
  });
431
- return this.#proposals.applyResolution(logEntryId, effective);
427
+ const post = await this.#proposals.applyResolution(logEntryId, effective);
428
+ await onSettled?.(logEntryId);
429
+ return post;
432
430
  }
433
431
  // Register the resolution waiter SYNCHRONOUSLY before any await
434
432
  // yields. A same-tick resolveProposal() (e.g. from a test that
@@ -448,6 +446,7 @@ class Dispatcher {
448
446
  }
449
447
  catch (cause) {
450
448
  await this.#proposals.failPreparation(logEntryId, cause);
449
+ await onSettled?.(logEntryId);
451
450
  throw cause;
452
451
  }
453
452
  const resolution = await resolutionPromise;
@@ -464,8 +463,10 @@ class Dispatcher {
464
463
  ids: { workspaceId, workerId, functionalityWorkerId, loopId, turnId },
465
464
  });
466
465
  const post = await this.#proposals.applyResolution(logEntryId, effective);
466
+ await onSettled?.(logEntryId);
467
467
  return post;
468
468
  }
469
+ await onSettled?.(logEntryId);
469
470
  return result;
470
471
  }
471
472
  // {§op-look}: resolve a READ and return its content without writing a
@@ -479,11 +480,14 @@ class Dispatcher {
479
480
  throw new Error(`look resolves READ only; got ${statement.op}`);
480
481
  // turnId is a write-time FK only — a look writes no row, so 0 (no turn) is inert.
481
482
  const schemeCtx = this.#buildSchemeCtx({ workspaceId, workerId, functionalityWorkerId: context.functionalityWorkerId, loopId, turnId: 0, origin });
482
- const denial = await this.#checkFlagsGate(statement, loopId, schemeCtx.functionalityWorkerId);
483
+ const denial = await this.#checkCapabilities(statement, schemeCtx.workspaceId, loopId, schemeCtx.functionalityWorkerId);
483
484
  if (denial !== null)
484
485
  return denial;
485
486
  return this.#run(schemeNameOf(statement.target), statement, schemeCtx);
486
487
  }
488
+ capabilityProjection(workspaceId, workerId) {
489
+ return this.#capabilities.projection(workspaceId, workerId);
490
+ }
487
491
  // Resolve the client selector through the owning scheme before persistence
488
492
  // is consulted. Public schemes choose a semantic owner; core-owned authority
489
493
  // schemes may return the already-authorized principal key.
@@ -531,7 +535,13 @@ class Dispatcher {
531
535
  }
532
536
  return this.#entryAddresses.resolve({ target, routedScheme, handler, manifest, ctx });
533
537
  }
534
- async #prepareDataRepresentation({ target, routedScheme, handler, manifest, ctx, publishedChannel, resolved: priorResolution, }) {
538
+ async #prepareDataRepresentation({ target, metadata, routedScheme, handler, manifest, ctx, publishedChannel, resolved: priorResolution, }) {
539
+ if (metadata !== null && manifest.metadataModifier !== true) {
540
+ return {
541
+ address: null,
542
+ result: _a.#failure("scheme-metadata-unsupported", 400, `Scheme '${routedScheme}' does not accept the {metadata} modifier.`, {}, { scheme: routedScheme, retryable: false }),
543
+ };
544
+ }
535
545
  const resolved = priorResolution ?? await this.#resolveDataEntryAddress({
536
546
  target, routedScheme, handler, manifest, ctx,
537
547
  });
@@ -555,6 +565,7 @@ class Dispatcher {
555
565
  });
556
566
  const prepared = Results.assertRepresentationPreparation(await handler.prepareRepresentation({
557
567
  target: selectionNeutralTarget,
568
+ metadata,
558
569
  authority: address.authority,
559
570
  pathname: address.pathname,
560
571
  }, preparationCtx));
@@ -598,7 +609,7 @@ class Dispatcher {
598
609
  // catalog fallback `body`; resolved through the same registry the writable gate reads.
599
610
  defaultChannelFor: (scheme) => this.#schemes.defaultChannelFor(scheme, functionalityWorkerId),
600
611
  settleDerivations: () => this.#settleDerivations(workspaceId),
601
- pushNotice: (notice) => this.#notices.push(workspaceId, loopId, notice),
612
+ pushNotice: (notice) => this.#notices.push(workspaceId, workerId, loopId, notice),
602
613
  requestInteraction: (request) => this.#interactions.request(request, { workspaceId, workerId, loopId, turnId }, this.#loopSignal(loopId)),
603
614
  executors: this.#executors(),
604
615
  };
@@ -620,19 +631,27 @@ class Dispatcher {
620
631
  if (statement.op === "EXEC") {
621
632
  return this.#denyIfDisallowed("exec", origin, workerId);
622
633
  }
634
+ // {§stream-control} — a KILL of a runtime stream terminates the caller's own process; it
635
+ // writes nothing into that read-only output scheme (writableBy plugin), so the writer rule
636
+ // does not speak. Exec.kill scopes the stream to the caller ({§stream-owner-scoped}).
637
+ if (statement.op === "KILL") {
638
+ const target = schemeNameOf(statement.target);
639
+ if (target !== null && this.#schemes.isRuntimeScheme(target, workerId))
640
+ return null;
641
+ }
623
642
  // Worker control (FORK/WORK → worker://<name>, spawn or fork) is gated by worker://'s writableBy — its
624
643
  // body is a seed prompt, not a dst path, so the entry-COPY dst-parse below doesn't apply.
625
644
  // {§machine-processes}
626
645
  if (this.#isWorkerControl(statement))
627
646
  return this.#denyIfDisallowed("worker", origin, workerId);
628
647
  if (statement.op === "COPY" || statement.op === "MOVE") {
629
- const dst = statement.body?.target ?? null;
648
+ const dst = statement.destination.target;
630
649
  const dstScheme = schemeNameOf(dst);
631
650
  const dstDenial = this.#denyIfDisallowed(dstScheme, origin, workerId);
632
651
  if (dstDenial !== null)
633
652
  return dstDenial;
634
653
  if (statement.op === "MOVE") {
635
- const srcScheme = schemeNameOf(statement.target);
654
+ const srcScheme = schemeNameOf(statement.source.target);
636
655
  if (srcScheme !== dstScheme) {
637
656
  const srcDenial = this.#denyIfDisallowed(srcScheme, origin, workerId);
638
657
  if (srcDenial !== null)
@@ -643,14 +662,14 @@ class Dispatcher {
643
662
  }
644
663
  const target = schemeNameOf(statement.target);
645
664
  const denial = this.#denyIfDisallowed(target, origin, workerId);
646
- // {§send-target-recipient} — a model's SEND to a scheme it may not write (the prompt,
647
- // the log) was meant as the reply; the writer rule is true and teaches nothing. Name the
648
- // form: the reply carries no target, a SEND target is a recipient (#367).
665
+ // {§send-target-recipient} — SEND addresses recipients, not otherwise
666
+ // read-only resources. State that boundary without guessing whether the
667
+ // model intended a reply, deletion, or directed message.
649
668
  if (denial !== null && statement.op === "SEND" && origin === "model") {
650
- return _a.#failure("send-target-not-a-recipient", 400, "`## SEND0 [200]` answers the active prompt with no target.", {}, {
669
+ return _a.#failure("send-target-not-a-recipient", 400, "The addressed scheme is not a SEND recipient.", {}, {
651
670
  target: statement.target?.raw ?? String(target),
652
671
  stage: "dispatch",
653
- recovery: "A SEND target is a recipient — `## SEND0 (worker://<name>)` — or, with `[410]`, a resource to delete.",
672
+ recovery: "A targetless SEND answers the active prompt; a directed SEND requires a recipient that implements SEND.",
654
673
  retryable: false,
655
674
  });
656
675
  }
@@ -674,92 +693,25 @@ class Dispatcher {
674
693
  retryable: false,
675
694
  }); // {§scheme-surface-writableby-403}
676
695
  }
677
- // Per-loop flag gating. Schemes self-declare their flag affinity in
678
- // their manifest (excludedInAsk / requiresWeb /
679
- // requiresInteraction); SchemeRegistry.resolveForLoop returns the
680
- // active set under the loop's persisted flags. A registered scheme outside
681
- // that set returns 403; unknown names continue to their operation owner for
682
- // the ordinary registration failure. Action-entry-as-outcome carries either.
683
- async #checkFlagsGate(statement, loopId, functionalityWorkerId) {
684
- const workerId = functionalityWorkerId;
685
- // Broadcast SEND has no scheme to gate.
686
- if (statement.op === "SEND" && statement.target === null)
687
- return null;
688
- const flags = await LoopFlagsReader.read(this.#db, loopId);
689
- // Fast path: default flags gate nothing. (auto never gates.)
690
- if (!flags.noWeb && !flags.noInteraction && flags.mode === "act")
696
+ // {§capability-admission} — one resolver gates every operation route before
697
+ // execution or proposal handling. Unknown routes continue to their ordinary
698
+ // owner; a known policy denial is one factual, non-presumptuous 403.
699
+ async #checkCapabilities(statement, workspaceId, loopId, functionalityWorkerId) {
700
+ const denied = await this.#capabilities.denial(statement, workspaceId, functionalityWorkerId, loopId);
701
+ if (denied === null)
691
702
  return null;
692
- // {§mode-ask-read-only} — the ancient contract: an ask-mode loop NEVER changes the world. The
693
- // filesystem writes (EDIT/COPY-dest/MOVE/KILL touching the `file` scheme — each proposes disk
694
- // egress, {§membership}) are refused HERE, regardless of the scheme's read-activity, because
695
- // `file` stays active for READs. The EXEC host runtime is refused by its excludedInAsk scheme
696
- // below. This lived only in SPEC (line 65) with no anchor → no guard → it silently regressed.
697
- if (flags.mode === "ask") {
698
- const isFile = (t) => schemeNameOf(t) === "file";
699
- // Each branch narrows statement.op so statement.body is correctly typed (COPY dest is a
700
- // resource selection). EDIT/KILL write the target; COPY writes the dest;
701
- // MOVE deletes the source AND writes the dest - any `file` touch is a write.
702
- let writesFilesystem = false;
703
- if (statement.op === "EDIT" || statement.op === "KILL")
704
- writesFilesystem = isFile(statement.target);
705
- else if (statement.op === "COPY")
706
- writesFilesystem = isFile(statement.body?.target ?? null);
707
- else if (statement.op === "MOVE")
708
- writesFilesystem = isFile(statement.target) || isFile(statement.body?.target ?? null);
709
- if (writesFilesystem) {
710
- return _a.#failure("ask-mode-read-only", 403, `${statement.op} cannot change the filesystem in an ask-mode loop.`, {}, {
711
- mode: flags.mode,
712
- operation: statement.op,
713
- recovery: "Answer or advise the user without changing the filesystem.",
714
- retryable: false,
715
- });
716
- }
717
- }
718
- const active = this.#schemes.resolveForLoop(flags, workerId);
719
- // {§mode-ask-read-only}: name the non-retryable restriction so the model changes course.
720
- const restriction = flags.mode === "ask"
721
- ? "this is an ask-mode (read-only) loop — you cannot run commands or take host actions here"
722
- : flags.noWeb && flags.noInteraction ? "web and interaction are disabled for this loop"
723
- : flags.noWeb ? "web access is disabled for this loop"
724
- : "interaction is disabled for this loop";
725
- const checkScheme = (scheme) => {
726
- if (scheme === null || !this.#schemes.has(scheme, workerId))
727
- return null;
728
- if (active.has(scheme))
729
- return null;
730
- return _a.#failure("scheme-unavailable", 403, `Scheme '${scheme}' is unavailable because ${restriction}.`, {}, {
731
- scheme,
732
- recovery: "Answer or advise the user without using the unavailable scheme.",
733
- retryable: false,
734
- });
735
- };
736
- const check = (target) => checkScheme(schemeNameOf(target));
737
- if (this.#isWorkerControl(statement))
738
- return check(statement.target); // body is a spawn/fork task, not a dst path
739
- if (statement.op === "COPY" || statement.op === "MOVE") {
740
- return check(statement.target) ?? check(statement.body?.target ?? null);
741
- }
742
- // {§exec-target-routing} — only a runtime-declared resource target adds
743
- // source authority. Literal identifiers and path targets stay executor-local.
744
- if (statement.op === "EXEC") {
745
- const operationDenial = checkScheme("exec");
746
- if (operationDenial !== null)
747
- return operationDenial;
748
- const requested = typeof statement.signal === "string" ? statement.signal : "";
749
- const runtime = requested === "" ? "sh" : requested;
750
- // {§manifest-flag-affinity} — a runtime alias is a registered scheme
751
- // with its own affinity: the one resolver gates the selected runtime
752
- // exactly as it gates the exec family (e.g. `question` under noInteraction).
753
- const runtimeDenial = checkScheme(runtime);
754
- if (runtimeDenial !== null)
755
- return runtimeDenial;
756
- const targetKind = this.#executors()?.entry(runtime, workerId)?.invocation.target?.kind;
757
- if (targetKind !== "resource")
758
- return null;
759
- const sourceScheme = schemeNameOf(statement.target);
760
- return sourceScheme === null || sourceScheme === "file" ? null : checkScheme(sourceScheme);
761
- }
762
- return check(statement.target);
703
+ const { descriptor, scope } = denied;
704
+ const route = [
705
+ descriptor.operation,
706
+ descriptor.scheme,
707
+ descriptor.runtime,
708
+ descriptor.tool,
709
+ ].filter((part) => part !== undefined).join("/");
710
+ return _a.#failure("capability-denied", 403, `Capability '${route}' is denied by ${scope} policy.`, {}, {
711
+ ...descriptor,
712
+ policyScope: scope,
713
+ retryable: false,
714
+ });
763
715
  }
764
716
  // Worker control is FORK/WORK (grammar 0.74.55), not COPY — its body
765
717
  // is the new worker's seed prompt, not a destination path. The COPY gates and ResourceMutations.handleCopy
@@ -793,10 +745,15 @@ class Dispatcher {
793
745
  if (denied !== null)
794
746
  return denied;
795
747
  const prompt = statement.body;
796
- // {§worker-delegation-inherits-flags} — authority flows down the delegation edge: the child's live
797
- // loop runs with ITS DELEGATOR'S flags. A flagless (non-auto) child's every side-effecting op
798
- // proposes into a resolver-less void — 300s auto-cancel per attempt was the fan-out wedge.
799
- const flags = await LoopFlagsReader.read(this.#db, ctx.loopId);
748
+ // {§worker-delegation-inherits-policy} — authority flows down the
749
+ // delegation edge. The child receives the delegator's complete loop
750
+ // policy and an immutable snapshot of its effective capability bound.
751
+ const policy = await LoopPolicyReader.read(this.#db, ctx.loopId);
752
+ const capabilityBound = await CapabilityPolicies.delegationBound(this.#db, ctx.workspaceId, ctx.workerId, policy);
753
+ const delegationPolicy = {
754
+ ...policy,
755
+ capabilities: capabilityBound,
756
+ };
800
757
  // A name is frozen per worker but reclaimable across time ({§machine-processes-worker-origin}): a LIVE
801
758
  // sister holding it is a 409 (legible, never a raw UNIQUE 500); a free/terminated name reclaims.
802
759
  const live = await this.#db.worker_live_by_name.get({ workspace_id: ctx.workspaceId, name });
@@ -804,6 +761,16 @@ class Dispatcher {
804
761
  return _a.#failure("worker-already-running", 409, `Worker '${name}' is already running.`, {}, { worker: name, retryable: false });
805
762
  }
806
763
  if (typeof statement.signal === "string") {
764
+ // {§branch-delegation-disabled} — off the model-facing surface until it is specified and
765
+ // witnessed end to end (#396); the batches stay behind the operator knob.
766
+ if (process.env.PLURNK_SERVICE_BRANCH_DELEGATION !== "1") {
767
+ return _a.#failure("branch-delegation-disabled", 501, `${statement.op} takes no signal; branch delegation is not offered.`, {}, {
768
+ worker: name,
769
+ signal: statement.signal,
770
+ recovery: `Spawn without a signal: \`## ${statement.op}0 (worker://${name})\` with the prompt as the body.`,
771
+ retryable: false,
772
+ });
773
+ }
807
774
  if (this.#branchWorker === undefined)
808
775
  throw new Error("branch worker control: branchWorker capability absent");
809
776
  const child = await this.#branchWorker({
@@ -815,7 +782,7 @@ class Dispatcher {
815
782
  name,
816
783
  branch: statement.signal,
817
784
  prompt,
818
- flags,
785
+ policy: delegationPolicy,
819
786
  origin: ctx.writer,
820
787
  });
821
788
  return {
@@ -826,13 +793,13 @@ class Dispatcher {
826
793
  }
827
794
  if (statement.op === "FORK") {
828
795
  // Branch the current worker's log into a named sister.
829
- const branchWorkerId = await Fork.fork(this.#db, ctx.workerId, name, (scheme) => this.#schemes.entryInheritanceForStoredScheme(scheme, ctx.workerId));
796
+ const branchWorkerId = await Fork.fork(this.#db, ctx.workerId, name, capabilityBound, (scheme) => this.#schemes.entryInheritanceForStoredScheme(scheme, ctx.workerId));
830
797
  await ctx.injectWorker({
831
798
  workspaceId: ctx.workspaceId,
832
799
  workerId: branchWorkerId,
833
800
  sourceWorkerId: ctx.workerId,
834
801
  prompt,
835
- flags,
802
+ freshLoopPolicy: delegationPolicy,
836
803
  parentLoopId: ctx.loopId,
837
804
  });
838
805
  return { status: 200, body: name };
@@ -841,6 +808,7 @@ class Dispatcher {
841
808
  const row = await this.#db.fork_insert_worker.get({
842
809
  workspace_id: ctx.workspaceId, name, parent_worker_id: ctx.workerId, origin: ctx.writer,
843
810
  fork_snapshot: 0,
811
+ capability_bound: JSON.stringify(capabilityBound),
844
812
  });
845
813
  if (row === undefined)
846
814
  throw new Error("worker spawn: worker insert returned no row");
@@ -849,7 +817,7 @@ class Dispatcher {
849
817
  workerId: row.id,
850
818
  sourceWorkerId: ctx.workerId,
851
819
  prompt,
852
- flags,
820
+ freshLoopPolicy: delegationPolicy,
853
821
  parentLoopId: ctx.loopId,
854
822
  });
855
823
  return { status: 200, body: name };
@@ -869,8 +837,8 @@ class Dispatcher {
869
837
  }
870
838
  const manifest = this.#schemes.manifestFor(schemeName, ctx.functionalityWorkerId);
871
839
  const coordinate = entryCoordinateOf(path, manifest?.authority ?? "namespace");
872
- // Log targets use the same killable dispatch as streams; erasure is their
873
- // permanent curation operation. {§turn-ops-log-curation}
840
+ // log:/// KILL has already gone through the projection-curation owner.
841
+ // This path owns scheme-specific world and process KILL semantics.
874
842
  // Process-KILL: any scheme whose handler exposes kill() aborts a live stream — the
875
843
  // exec handler, registered as "exec" + under every runtime tag (sh/node), so a tag-
876
844
  // addressed stream (sh:///l/t/s) routes here, not to deleteEntry. {§exec}
@@ -997,11 +965,10 @@ class Dispatcher {
997
965
  }, this.#weighContent),
998
966
  state: "resolved", outcome: null,
999
967
  attrs: JSON.stringify(durableAttrs),
968
+ initial_folded: LogVisibility.serialize(folded ? LogVisibility.FOLDED : LogVisibility.OPEN),
1000
969
  });
1001
970
  if (row === undefined)
1002
971
  throw new Error("Dispatcher.#writeActionlessEntry: insert returned no row");
1003
- if (folded)
1004
- await this.#db.engine_fold_log_entry.run({ id: row.id });
1005
972
  return row.id;
1006
973
  }
1007
974
  // {§turn-ops-entry} — preserve exact admitted source beside, never instead
@@ -1026,7 +993,7 @@ class Dispatcher {
1026
993
  },
1027
994
  });
1028
995
  }
1029
- // PLAN — the model's complete current Plan. An ordinary op: dispatched like any
996
+ // PLAN — one installment of the model's running work journal. An ordinary op: dispatched like any
1030
997
  // other, logged, and broadcast to the client as a log entry — but a pure no-op for
1031
998
  // state (PLAN ∉ MUTATING_OPS); its body serializes into the log row's tx, no effect.
1032
999
  #handlePlan(statement) {
@@ -1047,6 +1014,7 @@ class Dispatcher {
1047
1014
  modelCallId,
1048
1015
  });
1049
1016
  context.onDispatch?.(logEntryId);
1017
+ await context.onSettled?.(logEntryId);
1050
1018
  return result;
1051
1019
  }
1052
1020
  // {§send-premature-terminate} — the unified PENDING SET, judged at the terminal's OWN dispatch
@@ -1061,23 +1029,20 @@ class Dispatcher {
1061
1029
  const openSubs = await this.#db.find_open_subscriptions_for_worker.all({ worker_id: workerId });
1062
1030
  const execHandler = this.#schemes.get("exec");
1063
1031
  if (openSubs.length > 0 || execHandler?.hasActiveSpawns?.(workerId) === true)
1064
- pending.push("surviving streams");
1032
+ pending.push("streams");
1065
1033
  const liveChild = await this.#db.engine_worker_has_live_child.get({ worker_id: workerId });
1066
1034
  if (liveChild !== undefined)
1067
- pending.push("surviving workers");
1035
+ pending.push("workers");
1068
1036
  const boundaries = await this.#nextPacketBoundaries(workerId, turnId);
1069
1037
  if (boundaries.retrievals)
1070
- pending.push("this turn's receipts (READ/FIND/OPEN/BARE results and EDIT/COPY/MOVE effects land in the NEXT packet's Log)");
1038
+ pending.push("receipts");
1071
1039
  // A stream that closed successfully is banked, not pending: concluding on its own
1072
1040
  // success is legitimate and its output stays in the Log. A failed close is an unseen
1073
1041
  // failure — named exactly (bench#5 requiem #9) so the model reads it, not guesses.
1074
- const failedStreams = boundaries.streamTerminations
1075
- .filter(({ closeStatus }) => closeStatus >= 400)
1076
- .map(({ handle }) => handle);
1077
- if (failedStreams.length > 0)
1078
- pending.push(`failed stream results that land in the NEXT packet's Log: ${failedStreams.join("; ")}`);
1042
+ if (boundaries.streamTerminations.some(({ closeStatus }) => closeStatus >= 400))
1043
+ pending.push("failed-stream-results");
1079
1044
  if (boundaries.childTerminations)
1080
- pending.push("worker results that arrived during this turn (they land NEXT turn)");
1045
+ pending.push("worker-results");
1081
1046
  return pending;
1082
1047
  }
1083
1048
  // Results cross an observation boundary only when they have appeared in a packet;
@@ -1109,10 +1074,9 @@ class Dispatcher {
1109
1074
  return failedRows.length;
1110
1075
  }
1111
1076
  static #unobservedFailures(failCount) {
1112
- return _a.#failure("unobserved-failures", 409, `Completion was refused because ${failCount} failed operation(s) from this turn have not been observed.`, {}, {
1077
+ return _a.#failure("unobserved-failures", 409, `This turn produced ${failCount} failed operation result(s) that have not yet entered a packet.`, {}, {
1113
1078
  failures: failCount,
1114
1079
  stage: "completion",
1115
- recovery: "Review the failed log items before concluding.",
1116
1080
  retryable: false,
1117
1081
  });
1118
1082
  }
@@ -1165,7 +1129,8 @@ class Dispatcher {
1165
1129
  // retrieval (R) just lands next turn, so the wait continues.
1166
1130
  if (status === 202) {
1167
1131
  const marks = statement.lineMarker?.marks[0];
1168
- const seconds = typeof marks === "number" ? marks : -1; // bare 202 / absent T = indefinite, bounded by the join
1132
+ // `<T>` is MINUTES, held in seconds; bare 202 / absent T = indefinite, bounded by the join.
1133
+ const seconds = typeof marks === "number" ? (marks > 0 ? marks * 60 : marks) : -1;
1169
1134
  if (await this.#hasLiveWork(workerId)) {
1170
1135
  if (!await this.#lifecycle.park(loopId)) {
1171
1136
  return _a.#statusResult(await this.#lifecycle.status(loopId), "loop-already-terminal", "The loop was already terminal when SEND attempted to wait.");
@@ -1214,19 +1179,17 @@ class Dispatcher {
1214
1179
  if (pending.length > 0) {
1215
1180
  // A receipts-only refusal needs no KILL/park remedy menu: the results simply
1216
1181
  // arrive in the next packet. Streams and children retain their remedy steer.
1217
- const receiptsOnly = pending.every((k) => k.startsWith("this turn's receipts"));
1182
+ const receiptsOnly = pending.every((kind) => kind === "receipts");
1218
1183
  if (receiptsOnly) {
1219
- return _a.#failure("retrieval-results-unobserved", 409, "Last turn both performed operations whose receipts land in the next packet and attempted to terminate. Retrievals and mutations force an additional turn so their results can be reviewed.", {}, {
1184
+ return _a.#failure("retrieval-results-unobserved", 409, "Completion preceded this turn's operation results; they enter the next packet.", {}, {
1220
1185
  pending: [...pending],
1221
1186
  stage: "completion",
1222
- recovery: "Review the results, then use only `# PLAN0` and `## SEND0 [200]` to conclude.",
1223
1187
  retryable: false,
1224
1188
  });
1225
1189
  }
1226
- return _a.#failure("work-remains", 409, `Completion was refused while work remains: ${pending.join("; ")}.`, {}, {
1190
+ return _a.#failure("work-remains", 409, "Completion encountered pending work or results.", {}, {
1227
1191
  pending: [...pending],
1228
1192
  stage: "completion",
1229
- recovery: "Resolve the listed pending work before concluding.",
1230
1193
  retryable: false,
1231
1194
  });
1232
1195
  }
@@ -1241,7 +1204,11 @@ class Dispatcher {
1241
1204
  const branchDenial = await this.#branchCompletionGate?.(workerId) ?? null;
1242
1205
  if (branchDenial !== null)
1243
1206
  return branchDenial;
1244
- const failure = _a.#failure("scope-abandoned", 499, raw === "" ? "The worker abandoned its scope." : raw);
1207
+ const reason = raw === "" ? null : ErrorDetail.preview(raw);
1208
+ const failure = _a.#failure("scope-abandoned", 499, "The worker ended its scope with SEND[499].", {}, {
1209
+ ...(reason === null ? {} : { reason }),
1210
+ retryable: false,
1211
+ });
1245
1212
  const seqs = await this.#db.engine_loop_turn_seqs.get({
1246
1213
  loop_id: loopId,
1247
1214
  turn_id: turnId,
@@ -1253,7 +1220,7 @@ class Dispatcher {
1253
1220
  const finished = await this.#lifecycle.finish(loopId, failure);
1254
1221
  if (finished === null)
1255
1222
  return _a.#statusResult(await this.#lifecycle.status(loopId), "loop-already-terminal", "The loop was already terminal when SEND attempted to abandon it.");
1256
- await this.#cancelDescendants?.(workerId, raw === "" ? "parent abandoned its scope" : raw);
1223
+ await this.#cancelDescendants?.(workerId, reason ?? "parent worker ended its scope with SEND[499]");
1257
1224
  return failure;
1258
1225
  }
1259
1226
  // Every other signal — 102 bare, 202 (retired as a terminal; now ordinary mid-comms), 1xx —
@@ -1287,6 +1254,27 @@ class Dispatcher {
1287
1254
  const addressedScheme = statement.target?.kind === "url" ? statement.target.scheme : null;
1288
1255
  if (manifest === undefined)
1289
1256
  throw new Error(`scheme '${schemeName}' has no manifest`);
1257
+ // Metadata belongs to the handler that receives it. EXEC over a
1258
+ // non-file resource is the one split route: exec hosts the operation,
1259
+ // while the canonically routed source scheme receives the metadata.
1260
+ const metadataScheme = schemeName === "exec"
1261
+ && statement.target?.kind === "url"
1262
+ && statement.target.scheme !== "file"
1263
+ ? schemeNameOf(statement.target) ?? schemeName
1264
+ : schemeName;
1265
+ const metadataManifest = metadataScheme === schemeName
1266
+ ? manifest
1267
+ : this.#schemes.manifestFor(metadataScheme, ctx.functionalityWorkerId);
1268
+ if (statement.metadata !== null && metadataManifest === undefined) {
1269
+ return _a.#failure("scheme-not-found", 501, `Scheme '${metadataScheme}' is not registered.`, {}, { scheme: metadataScheme, retryable: false });
1270
+ }
1271
+ if (statement.metadata !== null && metadataManifest?.metadataModifier !== true) {
1272
+ return _a.#failure("scheme-metadata-unsupported", 400, `Scheme '${metadataScheme}' does not accept the {metadata} modifier.`, {}, {
1273
+ scheme: metadataScheme,
1274
+ operation: statement.op,
1275
+ retryable: false,
1276
+ });
1277
+ }
1290
1278
  const publishedChannel = statement.target?.kind === "url"
1291
1279
  ? statement.target.fragment ?? manifest.defaultChannel
1292
1280
  : manifest.defaultChannel;
@@ -1379,6 +1367,7 @@ class Dispatcher {
1379
1367
  ? { address: null, result: null }
1380
1368
  : await this.#prepareDataRepresentation({
1381
1369
  target: statement.target,
1370
+ metadata: statement.metadata,
1382
1371
  routedScheme: schemeName,
1383
1372
  handler: handler,
1384
1373
  manifest,
@@ -1412,10 +1401,6 @@ class Dispatcher {
1412
1401
  return _a.#failure("operation-not-implemented", 501, `Scheme '${schemeName}' does not implement ${statement.op}.`, {}, {
1413
1402
  scheme: schemeName,
1414
1403
  operation: statement.op,
1415
- // {§send-target-recipient} — a model's SEND to a file path was meant as the reply.
1416
- ...(statement.op === "SEND"
1417
- ? { recovery: "`## SEND0 [200]` answers the active prompt with no target. A SEND target is a recipient — `## SEND0 (worker://<name>)` — or, with `[410]`, a resource to delete." }
1418
- : {}),
1419
1404
  retryable: false,
1420
1405
  });
1421
1406
  }
@@ -1430,6 +1415,7 @@ class Dispatcher {
1430
1415
  if (exactTarget && statement.target !== null) {
1431
1416
  const prepared = await this.#prepareDataRepresentation({
1432
1417
  target: statement.target,
1418
+ metadata: statement.metadata,
1433
1419
  routedScheme: schemeName,
1434
1420
  handler: handler,
1435
1421
  manifest,
@@ -1504,10 +1490,11 @@ class Dispatcher {
1504
1490
  }
1505
1491
  async #writeLog({ statement, result, workspaceId, workerId, functionalityWorkerId, loopId, turnId, sequence, origin, curationPlan, modelCallId, }) {
1506
1492
  const durableStatement = DurableStatement.project(statement);
1507
- const target = this.#extractTarget(durableStatement.target, functionalityWorkerId);
1493
+ const target = this.#extractTarget(primaryTargetOf(durableStatement), functionalityWorkerId);
1508
1494
  await this.#canonColumns(target, workspaceId); // {§fs-answer-in-canon}
1509
- const lineMarkerJson = "lineMarker" in durableStatement && durableStatement.lineMarker !== null
1510
- ? JSON.stringify(durableStatement.lineMarker)
1495
+ const lineMarker = primaryLineMarkerOf(durableStatement);
1496
+ const lineMarkerJson = lineMarker !== null
1497
+ ? JSON.stringify(lineMarker)
1511
1498
  : null;
1512
1499
  // A proposal (status 202 from a side-effecting op) is written to the log in
1513
1500
  // state='proposed' until the proposal lifecycle resolves it; attrs holds the
@@ -1607,6 +1594,7 @@ class Dispatcher {
1607
1594
  state: isProposed ? "proposed" : "resolved",
1608
1595
  outcome: null,
1609
1596
  attrs,
1597
+ initial_folded: LogVisibility.serialize(LogVisibility.OPEN),
1610
1598
  });
1611
1599
  // {§exec-tag-signal} — an EXEC row's signal is its runtime; its tag signal classifies
1612
1600
  // through the same idempotent primitive the log-write trigger uses for tag operations.