@plurnk/plurnk-service 1.10.1 → 1.12.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 (314) hide show
  1. package/.env.defaults +41 -17
  2. package/INSTALL.md +4 -4
  3. package/README.md +2 -2
  4. package/SPEC.md +573 -273
  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.d.ts +1 -1
  12. package/dist/content/edit-collision.d.ts.map +1 -1
  13. package/dist/content/edit-collision.js +5 -4
  14. package/dist/content/edit-collision.js.map +1 -1
  15. package/dist/content/edit-receipt.d.ts +6 -5
  16. package/dist/content/edit-receipt.d.ts.map +1 -1
  17. package/dist/content/edit-receipt.js +28 -8
  18. package/dist/content/edit-receipt.js.map +1 -1
  19. package/dist/content/index.d.ts +2 -2
  20. package/dist/content/index.d.ts.map +1 -1
  21. package/dist/content/index.js +1 -1
  22. package/dist/content/index.js.map +1 -1
  23. package/dist/content/line-anchors.d.ts +2 -0
  24. package/dist/content/line-anchors.d.ts.map +1 -1
  25. package/dist/content/line-anchors.js +15 -6
  26. package/dist/content/line-anchors.js.map +1 -1
  27. package/dist/content/line-marker.d.ts +0 -1
  28. package/dist/content/line-marker.d.ts.map +1 -1
  29. package/dist/content/line-marker.js.map +1 -1
  30. package/dist/content/matcher.d.ts.map +1 -1
  31. package/dist/content/matcher.js +3 -2
  32. package/dist/content/matcher.js.map +1 -1
  33. package/dist/content/read-projector.js +3 -3
  34. package/dist/content/read-projector.js.map +1 -1
  35. package/dist/core/BudgetReadout.d.ts +6 -1
  36. package/dist/core/BudgetReadout.d.ts.map +1 -1
  37. package/dist/core/BudgetReadout.js +38 -2
  38. package/dist/core/BudgetReadout.js.map +1 -1
  39. package/dist/core/CapabilityPolicies.d.ts +15 -0
  40. package/dist/core/CapabilityPolicies.d.ts.map +1 -0
  41. package/dist/core/CapabilityPolicies.js +60 -0
  42. package/dist/core/CapabilityPolicies.js.map +1 -0
  43. package/dist/core/CapabilityResolver.d.ts +24 -0
  44. package/dist/core/CapabilityResolver.d.ts.map +1 -0
  45. package/dist/core/CapabilityResolver.js +180 -0
  46. package/dist/core/CapabilityResolver.js.map +1 -0
  47. package/dist/core/ChannelWrite.d.ts +4 -3
  48. package/dist/core/ChannelWrite.d.ts.map +1 -1
  49. package/dist/core/CoreSchemeServices.d.ts +2 -2
  50. package/dist/core/CoreSchemeServices.d.ts.map +1 -1
  51. package/dist/core/CoreSchemeServices.js +2 -2
  52. package/dist/core/CoreSchemeServices.js.map +1 -1
  53. package/dist/core/Dispatcher.d.ts +5 -2
  54. package/dist/core/Dispatcher.d.ts.map +1 -1
  55. package/dist/core/Dispatcher.js +157 -165
  56. package/dist/core/Dispatcher.js.map +1 -1
  57. package/dist/core/DurableStatement.d.ts.map +1 -1
  58. package/dist/core/DurableStatement.js +19 -15
  59. package/dist/core/DurableStatement.js.map +1 -1
  60. package/dist/core/EmbeddingCall.d.ts +23 -0
  61. package/dist/core/EmbeddingCall.d.ts.map +1 -0
  62. package/dist/core/EmbeddingCall.js +149 -0
  63. package/dist/core/EmbeddingCall.js.map +1 -0
  64. package/dist/core/Engine.d.ts +6 -4
  65. package/dist/core/Engine.d.ts.map +1 -1
  66. package/dist/core/Engine.js +20 -9
  67. package/dist/core/Engine.js.map +1 -1
  68. package/dist/core/Engine.sql +119 -56
  69. package/dist/core/ExecutorRegistry.d.ts +3 -3
  70. package/dist/core/ExecutorRegistry.d.ts.map +1 -1
  71. package/dist/core/ExecutorRegistry.js +10 -6
  72. package/dist/core/ExecutorRegistry.js.map +1 -1
  73. package/dist/core/GitBranch.d.ts +4 -1
  74. package/dist/core/GitBranch.d.ts.map +1 -1
  75. package/dist/core/GitBranch.js +12 -2
  76. package/dist/core/GitBranch.js.map +1 -1
  77. package/dist/core/InferenceCall.d.ts +18 -0
  78. package/dist/core/InferenceCall.d.ts.map +1 -0
  79. package/dist/core/InferenceCall.js +86 -0
  80. package/dist/core/InferenceCall.js.map +1 -0
  81. package/dist/core/LogEntryProjection.d.ts +1 -0
  82. package/dist/core/LogEntryProjection.d.ts.map +1 -1
  83. package/dist/core/LogEntryProjection.js +12 -4
  84. package/dist/core/LogEntryProjection.js.map +1 -1
  85. package/dist/core/LoopPolicyReader.d.ts +7 -0
  86. package/dist/core/LoopPolicyReader.d.ts.map +1 -0
  87. package/dist/core/LoopPolicyReader.js +33 -0
  88. package/dist/core/LoopPolicyReader.js.map +1 -0
  89. package/dist/core/ModelCall.d.ts +4 -12
  90. package/dist/core/ModelCall.d.ts.map +1 -1
  91. package/dist/core/ModelCall.js +10 -74
  92. package/dist/core/ModelCall.js.map +1 -1
  93. package/dist/core/NoticeChannel.d.ts +2 -2
  94. package/dist/core/NoticeChannel.d.ts.map +1 -1
  95. package/dist/core/NoticeChannel.js +18 -9
  96. package/dist/core/NoticeChannel.js.map +1 -1
  97. package/dist/core/OperatorConfig.d.ts.map +1 -1
  98. package/dist/core/OperatorConfig.js +4 -0
  99. package/dist/core/OperatorConfig.js.map +1 -1
  100. package/dist/core/OverflowTurn.d.ts.map +1 -1
  101. package/dist/core/OverflowTurn.js +4 -3
  102. package/dist/core/OverflowTurn.js.map +1 -1
  103. package/dist/core/Owner.js +5 -5
  104. package/dist/core/Owner.js.map +1 -1
  105. package/dist/core/PacketBuilder.d.ts +2 -2
  106. package/dist/core/PacketBuilder.d.ts.map +1 -1
  107. package/dist/core/PacketBuilder.js +55 -35
  108. package/dist/core/PacketBuilder.js.map +1 -1
  109. package/dist/core/ProblemLog.d.ts.map +1 -1
  110. package/dist/core/ProblemLog.js +1 -0
  111. package/dist/core/ProblemLog.js.map +1 -1
  112. package/dist/core/ProposalLifecycle.d.ts.map +1 -1
  113. package/dist/core/ProposalLifecycle.js +16 -13
  114. package/dist/core/ProposalLifecycle.js.map +1 -1
  115. package/dist/core/ReasoningEvent.d.ts +1 -0
  116. package/dist/core/ReasoningEvent.d.ts.map +1 -1
  117. package/dist/core/ResourceMutations.d.ts +6 -4
  118. package/dist/core/ResourceMutations.d.ts.map +1 -1
  119. package/dist/core/ResourceMutations.js +209 -63
  120. package/dist/core/ResourceMutations.js.map +1 -1
  121. package/dist/core/SchemeRegistry.d.ts +6 -3
  122. package/dist/core/SchemeRegistry.d.ts.map +1 -1
  123. package/dist/core/SchemeRegistry.js +14 -17
  124. package/dist/core/SchemeRegistry.js.map +1 -1
  125. package/dist/core/ServiceTeardown.d.ts +1 -1
  126. package/dist/core/ServiceTeardown.d.ts.map +1 -1
  127. package/dist/core/ServiceTeardown.js +4 -8
  128. package/dist/core/ServiceTeardown.js.map +1 -1
  129. package/dist/core/StrikeRail.d.ts +1 -0
  130. package/dist/core/StrikeRail.d.ts.map +1 -1
  131. package/dist/core/StrikeRail.js +12 -3
  132. package/dist/core/StrikeRail.js.map +1 -1
  133. package/dist/core/ToolResources.d.ts +2 -2
  134. package/dist/core/ToolResources.d.ts.map +1 -1
  135. package/dist/core/ToolResources.js +18 -8
  136. package/dist/core/ToolResources.js.map +1 -1
  137. package/dist/core/Turn.sql +6 -3
  138. package/dist/core/TurnOps.d.ts.map +1 -1
  139. package/dist/core/TurnOps.js +22 -8
  140. package/dist/core/TurnOps.js.map +1 -1
  141. package/dist/core/TurnRunner.d.ts +7 -4
  142. package/dist/core/TurnRunner.d.ts.map +1 -1
  143. package/dist/core/TurnRunner.js +345 -83
  144. package/dist/core/TurnRunner.js.map +1 -1
  145. package/dist/core/WorkerControlAddress.d.ts.map +1 -1
  146. package/dist/core/WorkerControlAddress.js +1 -2
  147. package/dist/core/WorkerControlAddress.js.map +1 -1
  148. package/dist/core/WorkerName.d.ts +2 -0
  149. package/dist/core/WorkerName.d.ts.map +1 -1
  150. package/dist/core/WorkerName.js +3 -2
  151. package/dist/core/WorkerName.js.map +1 -1
  152. package/dist/core/WorkerName.sql +2 -1
  153. package/dist/core/caps/DbProjectionCaps.d.ts +2 -1
  154. package/dist/core/caps/DbProjectionCaps.d.ts.map +1 -1
  155. package/dist/core/caps/DbProjectionCaps.js +12 -1
  156. package/dist/core/caps/DbProjectionCaps.js.map +1 -1
  157. package/dist/core/file-materialization.d.ts +22 -0
  158. package/dist/core/file-materialization.d.ts.map +1 -0
  159. package/dist/core/file-materialization.js +77 -0
  160. package/dist/core/file-materialization.js.map +1 -0
  161. package/dist/core/fork.d.ts +2 -1
  162. package/dist/core/fork.d.ts.map +1 -1
  163. package/dist/core/fork.js +13 -4
  164. package/dist/core/fork.js.map +1 -1
  165. package/dist/core/fork.sql +27 -11
  166. package/dist/core/git-membership.d.ts +25 -14
  167. package/dist/core/git-membership.d.ts.map +1 -1
  168. package/dist/core/git-membership.js +260 -164
  169. package/dist/core/git-membership.js.map +1 -1
  170. package/dist/core/git-state.d.ts +2 -0
  171. package/dist/core/git-state.d.ts.map +1 -1
  172. package/dist/core/git-state.js +27 -1
  173. package/dist/core/git-state.js.map +1 -1
  174. package/dist/core/operation-target-groups.d.ts.map +1 -1
  175. package/dist/core/operation-target-groups.js +8 -7
  176. package/dist/core/operation-target-groups.js.map +1 -1
  177. package/dist/core/owner.sql +7 -10
  178. package/dist/core/packet-wire.d.ts +11 -0
  179. package/dist/core/packet-wire.d.ts.map +1 -1
  180. package/dist/core/packet-wire.js +98 -34
  181. package/dist/core/packet-wire.js.map +1 -1
  182. package/dist/core/scheme-types.d.ts +2 -2
  183. package/dist/core/scheme-types.d.ts.map +1 -1
  184. package/dist/core/scheme-types.js +1 -1
  185. package/dist/core/scheme-types.js.map +1 -1
  186. package/dist/core/types.d.ts +3 -2
  187. package/dist/core/types.d.ts.map +1 -1
  188. package/dist/core/types.js +1 -1
  189. package/dist/core/types.js.map +1 -1
  190. package/dist/core/worker-settings.d.ts +4 -3
  191. package/dist/core/worker-settings.d.ts.map +1 -1
  192. package/dist/core/worker-settings.js +40 -19
  193. package/dist/core/worker-settings.js.map +1 -1
  194. package/dist/core/workspace-settings.d.ts +3 -1
  195. package/dist/core/workspace-settings.d.ts.map +1 -1
  196. package/dist/core/workspace-settings.js +6 -2
  197. package/dist/core/workspace-settings.js.map +1 -1
  198. package/dist/digest/Digest.d.ts.map +1 -1
  199. package/dist/digest/Digest.js +151 -42
  200. package/dist/digest/Digest.js.map +1 -1
  201. package/dist/digest/digest.sql +48 -20
  202. package/dist/index.js +1 -1
  203. package/dist/index.js.map +1 -1
  204. package/dist/schemes/Exec.d.ts +4 -5
  205. package/dist/schemes/Exec.d.ts.map +1 -1
  206. package/dist/schemes/Exec.js +111 -94
  207. package/dist/schemes/Exec.js.map +1 -1
  208. package/dist/schemes/ExecOutputScheme.d.ts.map +1 -1
  209. package/dist/schemes/ExecOutputScheme.js +24 -6
  210. package/dist/schemes/ExecOutputScheme.js.map +1 -1
  211. package/dist/schemes/ExecScheduler.d.ts +15 -0
  212. package/dist/schemes/ExecScheduler.d.ts.map +1 -0
  213. package/dist/schemes/ExecScheduler.js +95 -0
  214. package/dist/schemes/ExecScheduler.js.map +1 -0
  215. package/dist/schemes/File.d.ts.map +1 -1
  216. package/dist/schemes/File.js +26 -19
  217. package/dist/schemes/File.js.map +1 -1
  218. package/dist/schemes/Log.d.ts +6 -4
  219. package/dist/schemes/Log.d.ts.map +1 -1
  220. package/dist/schemes/Log.js +132 -39
  221. package/dist/schemes/Log.js.map +1 -1
  222. package/dist/schemes/Log.sql +23 -18
  223. package/dist/schemes/QuestionTool.d.ts +17 -4
  224. package/dist/schemes/QuestionTool.d.ts.map +1 -1
  225. package/dist/schemes/QuestionTool.js +2 -4
  226. package/dist/schemes/QuestionTool.js.map +1 -1
  227. package/dist/schemes/Worker.d.ts.map +1 -1
  228. package/dist/schemes/Worker.js +20 -20
  229. package/dist/schemes/Worker.js.map +1 -1
  230. package/dist/schemes/_entry-chunk.d.ts.map +1 -1
  231. package/dist/schemes/_entry-chunk.js +6 -2
  232. package/dist/schemes/_entry-chunk.js.map +1 -1
  233. package/dist/schemes/_entry-crud.sql +18 -17
  234. package/dist/schemes/_entry-find.d.ts.map +1 -1
  235. package/dist/schemes/_entry-find.js +9 -14
  236. package/dist/schemes/_entry-find.js.map +1 -1
  237. package/dist/schemes/_entry-graph.js +12 -12
  238. package/dist/schemes/_entry-graph.js.map +1 -1
  239. package/dist/schemes/_entry-graph.sql +5 -5
  240. package/dist/schemes/_entry-ops.d.ts.map +1 -1
  241. package/dist/schemes/_entry-ops.js +15 -5
  242. package/dist/schemes/_entry-ops.js.map +1 -1
  243. package/dist/schemes/_entry-semantic.d.ts +5 -3
  244. package/dist/schemes/_entry-semantic.d.ts.map +1 -1
  245. package/dist/schemes/_entry-semantic.js +21 -9
  246. package/dist/schemes/_entry-semantic.js.map +1 -1
  247. package/dist/schemes/_path-scope.d.ts.map +1 -1
  248. package/dist/schemes/_path-scope.js +6 -1
  249. package/dist/schemes/_path-scope.js.map +1 -1
  250. package/dist/schemes/_search-index.d.ts.map +1 -1
  251. package/dist/schemes/_search-index.js +14 -2
  252. package/dist/schemes/_search-index.js.map +1 -1
  253. package/dist/server/BranchBatches.d.ts +3 -3
  254. package/dist/server/BranchBatches.d.ts.map +1 -1
  255. package/dist/server/BranchBatches.js +9 -2
  256. package/dist/server/BranchBatches.js.map +1 -1
  257. package/dist/server/Daemon.d.ts +12 -41
  258. package/dist/server/Daemon.d.ts.map +1 -1
  259. package/dist/server/Daemon.js +60 -93
  260. package/dist/server/Daemon.js.map +1 -1
  261. package/dist/server/DaemonModule.d.ts +10 -1
  262. package/dist/server/DaemonModule.d.ts.map +1 -1
  263. package/dist/server/DrainSupervisor.d.ts +6 -5
  264. package/dist/server/DrainSupervisor.d.ts.map +1 -1
  265. package/dist/server/DrainSupervisor.js +14 -11
  266. package/dist/server/DrainSupervisor.js.map +1 -1
  267. package/dist/server/Functionality.d.ts +2 -2
  268. package/dist/server/Functionality.d.ts.map +1 -1
  269. package/dist/server/Functionality.js +8 -5
  270. package/dist/server/Functionality.js.map +1 -1
  271. package/dist/server/FunctionalityManager.d.ts +13 -1
  272. package/dist/server/FunctionalityManager.d.ts.map +1 -1
  273. package/dist/server/FunctionalityManager.js +82 -17
  274. package/dist/server/FunctionalityManager.js.map +1 -1
  275. package/dist/server/MembersFunctionality.d.ts +56 -0
  276. package/dist/server/MembersFunctionality.d.ts.map +1 -0
  277. package/dist/server/MembersFunctionality.js +350 -0
  278. package/dist/server/MembersFunctionality.js.map +1 -0
  279. package/dist/server/SkillsFunctionality.d.ts +12 -1
  280. package/dist/server/SkillsFunctionality.d.ts.map +1 -1
  281. package/dist/server/SkillsFunctionality.js +6 -1
  282. package/dist/server/SkillsFunctionality.js.map +1 -1
  283. package/dist/server/client-input.d.ts +5 -8
  284. package/dist/server/client-input.d.ts.map +1 -1
  285. package/dist/server/client-input.js +49 -108
  286. package/dist/server/client-input.js.map +1 -1
  287. package/dist/server/dispatch-as-plurnk.d.ts.map +1 -1
  288. package/dist/server/dispatch-as-plurnk.js +3 -0
  289. package/dist/server/dispatch-as-plurnk.js.map +1 -1
  290. package/dist/server/drain.sql +4 -4
  291. package/dist/server/envelope.d.ts +1 -1
  292. package/dist/server/envelope.d.ts.map +1 -1
  293. package/dist/server/envelope.js +11 -10
  294. package/dist/server/envelope.js.map +1 -1
  295. package/dist/server/envelope.sql +1 -1
  296. package/dist/server/lifecycle-recovery.sql +14 -20
  297. package/dist/server/loopDocs.d.ts.map +1 -1
  298. package/dist/server/loopDocs.js +12 -5
  299. package/dist/server/loopDocs.js.map +1 -1
  300. package/dist/server/seam-proposal-list.sql +2 -2
  301. package/dist/server/worker-capabilities.sql +9 -0
  302. package/dist/service.d.ts.map +1 -1
  303. package/dist/service.js +18 -23
  304. package/dist/service.js.map +1 -1
  305. package/migrations/001_schema.sql +477 -131
  306. package/package.json +31 -33
  307. package/dist/core/LoopFlagsReader.d.ts +0 -7
  308. package/dist/core/LoopFlagsReader.d.ts.map +0 -1
  309. package/dist/core/LoopFlagsReader.js +0 -33
  310. package/dist/core/LoopFlagsReader.js.map +0 -1
  311. package/dist/core/resolveForLoop.d.ts +0 -5
  312. package/dist/core/resolveForLoop.d.ts.map +0 -1
  313. package/dist/core/resolveForLoop.js +0 -12
  314. 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({
@@ -283,21 +290,24 @@ class Dispatcher {
283
290
  }
284
291
  async dispatch(context) {
285
292
  assertClassifyingSignal(context.statement); // {§log-tag-signal}
286
- const result = await this.#dispatchOne(context);
293
+ let result = await this.#dispatchOne(context);
287
294
  if (context.statement.op === "EDIT") {
295
+ // {§edit-batch-merges} — a proposal's apply result replaces the projected one; the
296
+ // statement's merge facts ride every EDIT result, whichever route produced it.
297
+ result = this.#resourceMutations.withMergeFacts(context.statement, result);
288
298
  this.#resourceMutations.settleEdit(context.statement, result);
289
299
  }
290
300
  return result;
291
301
  }
292
302
  async #dispatchOne(context) {
293
- const { statement, workspaceId, workerId, loopId, turnId, sequence, origin, onDispatch, } = context;
303
+ const { statement, workspaceId, workerId, loopId, turnId, sequence, origin, onDispatch, onSettled, } = context;
294
304
  const schemeCtx = this.#buildSchemeCtx({ workspaceId, workerId, functionalityWorkerId: context.functionalityWorkerId, loopId, turnId, origin });
295
305
  const { functionalityWorkerId } = schemeCtx;
296
306
  let result;
297
307
  let curationPlan = null;
298
308
  let denial = this.#checkWritable(statement, origin, functionalityWorkerId);
299
309
  if (denial === null)
300
- denial = await this.#checkFlagsGate(statement, loopId, functionalityWorkerId);
310
+ denial = await this.#checkCapabilities(statement, workspaceId, loopId, functionalityWorkerId);
301
311
  if (denial !== null) {
302
312
  result = denial;
303
313
  }
@@ -321,7 +331,9 @@ class Dispatcher {
321
331
  origin,
322
332
  });
323
333
  }
324
- else if (statement.op === "OPEN" || statement.op === "FOLD") {
334
+ else if (statement.op === "OPEN"
335
+ || statement.op === "FOLD"
336
+ || (statement.op === "KILL" && schemeNameOf(statement.target) === "log")) {
325
337
  const curation = await this.#runLogCuration(statement, schemeCtx);
326
338
  result = curation.result;
327
339
  curationPlan = curation.plan;
@@ -342,22 +354,9 @@ class Dispatcher {
342
354
  result = this.#handlePlan(statement);
343
355
  }
344
356
  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
- }
357
+ // EXEC routes unconditionally to its operation owner after
358
+ // the shared capability resolver admits its runtime/tool.
359
+ result = await this.#run("exec", statement, schemeCtx);
361
360
  }
362
361
  else {
363
362
  result = await this.#run(schemeNameOf(statement.target), statement, schemeCtx); // {§op-methods-op-dispatch}
@@ -370,7 +369,7 @@ class Dispatcher {
370
369
  result = err.result;
371
370
  }
372
371
  else {
373
- const scheme = schemeNameOf(statement.target);
372
+ const scheme = schemeNameOf(primaryTargetOf(statement));
374
373
  console.error(`Scheme '${scheme ?? "unknown"}' ${statement.op} threw outside its operation result contract:`, err);
375
374
  result = _a.#failure("scheme-handler-threw", 500, `The '${scheme ?? "unknown"}' scheme did not produce a ${statement.op} result.`, {}, {
376
375
  stage: "scheme-dispatch",
@@ -380,8 +379,8 @@ class Dispatcher {
380
379
  }
381
380
  }
382
381
  }
383
- // {§fold-open-meta-operations} — persist OPEN/FOLD for forensics;
384
- // packet rendering suppresses their successful receipts.
382
+ // Persist log curation for forensics; packet rendering suppresses its
383
+ // successful receipts while the exact state effects remain durable.
385
384
  // A running-worker READ arms this turn's blocking collect.
386
385
  // {§join-blocking-collect}
387
386
  if (typeof result.awaitWorker === "string")
@@ -428,7 +427,9 @@ class Dispatcher {
428
427
  ctx: schemeCtx,
429
428
  ids: { workspaceId, workerId, functionalityWorkerId, loopId, turnId },
430
429
  });
431
- return this.#proposals.applyResolution(logEntryId, effective);
430
+ const post = await this.#proposals.applyResolution(logEntryId, effective);
431
+ await onSettled?.(logEntryId);
432
+ return post;
432
433
  }
433
434
  // Register the resolution waiter SYNCHRONOUSLY before any await
434
435
  // yields. A same-tick resolveProposal() (e.g. from a test that
@@ -448,6 +449,7 @@ class Dispatcher {
448
449
  }
449
450
  catch (cause) {
450
451
  await this.#proposals.failPreparation(logEntryId, cause);
452
+ await onSettled?.(logEntryId);
451
453
  throw cause;
452
454
  }
453
455
  const resolution = await resolutionPromise;
@@ -464,8 +466,10 @@ class Dispatcher {
464
466
  ids: { workspaceId, workerId, functionalityWorkerId, loopId, turnId },
465
467
  });
466
468
  const post = await this.#proposals.applyResolution(logEntryId, effective);
469
+ await onSettled?.(logEntryId);
467
470
  return post;
468
471
  }
472
+ await onSettled?.(logEntryId);
469
473
  return result;
470
474
  }
471
475
  // {§op-look}: resolve a READ and return its content without writing a
@@ -479,11 +483,14 @@ class Dispatcher {
479
483
  throw new Error(`look resolves READ only; got ${statement.op}`);
480
484
  // turnId is a write-time FK only — a look writes no row, so 0 (no turn) is inert.
481
485
  const schemeCtx = this.#buildSchemeCtx({ workspaceId, workerId, functionalityWorkerId: context.functionalityWorkerId, loopId, turnId: 0, origin });
482
- const denial = await this.#checkFlagsGate(statement, loopId, schemeCtx.functionalityWorkerId);
486
+ const denial = await this.#checkCapabilities(statement, schemeCtx.workspaceId, loopId, schemeCtx.functionalityWorkerId);
483
487
  if (denial !== null)
484
488
  return denial;
485
489
  return this.#run(schemeNameOf(statement.target), statement, schemeCtx);
486
490
  }
491
+ capabilityProjection(workspaceId, workerId) {
492
+ return this.#capabilities.projection(workspaceId, workerId);
493
+ }
487
494
  // Resolve the client selector through the owning scheme before persistence
488
495
  // is consulted. Public schemes choose a semantic owner; core-owned authority
489
496
  // schemes may return the already-authorized principal key.
@@ -531,7 +538,13 @@ class Dispatcher {
531
538
  }
532
539
  return this.#entryAddresses.resolve({ target, routedScheme, handler, manifest, ctx });
533
540
  }
534
- async #prepareDataRepresentation({ target, routedScheme, handler, manifest, ctx, publishedChannel, resolved: priorResolution, }) {
541
+ async #prepareDataRepresentation({ target, metadata, routedScheme, handler, manifest, ctx, publishedChannel, resolved: priorResolution, }) {
542
+ if (metadata !== null && manifest.metadataModifier !== true) {
543
+ return {
544
+ address: null,
545
+ result: _a.#failure("scheme-metadata-unsupported", 400, `Scheme '${routedScheme}' does not accept the {metadata} modifier.`, {}, { scheme: routedScheme, retryable: false }),
546
+ };
547
+ }
535
548
  const resolved = priorResolution ?? await this.#resolveDataEntryAddress({
536
549
  target, routedScheme, handler, manifest, ctx,
537
550
  });
@@ -555,6 +568,7 @@ class Dispatcher {
555
568
  });
556
569
  const prepared = Results.assertRepresentationPreparation(await handler.prepareRepresentation({
557
570
  target: selectionNeutralTarget,
571
+ metadata,
558
572
  authority: address.authority,
559
573
  pathname: address.pathname,
560
574
  }, preparationCtx));
@@ -584,7 +598,7 @@ class Dispatcher {
584
598
  #buildSchemeCtx(ids) {
585
599
  const { workspaceId, workerId, loopId, turnId, origin } = ids;
586
600
  const functionalityWorkerId = ids.functionalityWorkerId ?? workerId;
587
- return {
601
+ const context = {
588
602
  db: this.#db,
589
603
  workspaceId, workerId, functionalityWorkerId, loopId, turnId,
590
604
  writer: origin,
@@ -597,11 +611,12 @@ class Dispatcher {
597
611
  // {§exec-stream} — a runtime scheme's default channel is its own (stdout), never the
598
612
  // catalog fallback `body`; resolved through the same registry the writable gate reads.
599
613
  defaultChannelFor: (scheme) => this.#schemes.defaultChannelFor(scheme, functionalityWorkerId),
600
- settleDerivations: () => this.#settleDerivations(workspaceId),
601
- pushNotice: (notice) => this.#notices.push(workspaceId, loopId, notice),
614
+ settleDerivations: () => this.#settleDerivations(context),
615
+ pushNotice: (notice) => this.#notices.push(workspaceId, workerId, loopId, notice),
602
616
  requestInteraction: (request) => this.#interactions.request(request, { workspaceId, workerId, loopId, turnId }, this.#loopSignal(loopId)),
603
617
  executors: this.#executors(),
604
618
  };
619
+ return context;
605
620
  }
606
621
  // SPEC {§scheme-surface}: engine rejects writes whose origin is outside the target
607
622
  // scheme's manifest.writableBy.
@@ -620,19 +635,27 @@ class Dispatcher {
620
635
  if (statement.op === "EXEC") {
621
636
  return this.#denyIfDisallowed("exec", origin, workerId);
622
637
  }
638
+ // {§stream-control} — a KILL of a runtime stream terminates the caller's own process; it
639
+ // writes nothing into that read-only output scheme (writableBy plugin), so the writer rule
640
+ // does not speak. Exec.kill scopes the stream to the caller ({§stream-owner-scoped}).
641
+ if (statement.op === "KILL") {
642
+ const target = schemeNameOf(statement.target);
643
+ if (target !== null && this.#schemes.isRuntimeScheme(target, workerId))
644
+ return null;
645
+ }
623
646
  // Worker control (FORK/WORK → worker://<name>, spawn or fork) is gated by worker://'s writableBy — its
624
647
  // body is a seed prompt, not a dst path, so the entry-COPY dst-parse below doesn't apply.
625
648
  // {§machine-processes}
626
649
  if (this.#isWorkerControl(statement))
627
650
  return this.#denyIfDisallowed("worker", origin, workerId);
628
651
  if (statement.op === "COPY" || statement.op === "MOVE") {
629
- const dst = statement.body?.target ?? null;
652
+ const dst = statement.destination.target;
630
653
  const dstScheme = schemeNameOf(dst);
631
654
  const dstDenial = this.#denyIfDisallowed(dstScheme, origin, workerId);
632
655
  if (dstDenial !== null)
633
656
  return dstDenial;
634
657
  if (statement.op === "MOVE") {
635
- const srcScheme = schemeNameOf(statement.target);
658
+ const srcScheme = schemeNameOf(statement.source.target);
636
659
  if (srcScheme !== dstScheme) {
637
660
  const srcDenial = this.#denyIfDisallowed(srcScheme, origin, workerId);
638
661
  if (srcDenial !== null)
@@ -643,14 +666,14 @@ class Dispatcher {
643
666
  }
644
667
  const target = schemeNameOf(statement.target);
645
668
  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).
669
+ // {§send-target-recipient} — SEND addresses recipients, not otherwise
670
+ // read-only resources. State that boundary without guessing whether the
671
+ // model intended a reply, deletion, or directed message.
649
672
  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.", {}, {
673
+ return _a.#failure("send-target-not-a-recipient", 400, "The addressed scheme is not a SEND recipient.", {}, {
651
674
  target: statement.target?.raw ?? String(target),
652
675
  stage: "dispatch",
653
- recovery: "A SEND target is a recipient — `## SEND0 (worker://<name>)` — or, with `[410]`, a resource to delete.",
676
+ recovery: "A targetless SEND answers the active prompt; a directed SEND requires a recipient that implements SEND.",
654
677
  retryable: false,
655
678
  });
656
679
  }
@@ -674,92 +697,25 @@ class Dispatcher {
674
697
  retryable: false,
675
698
  }); // {§scheme-surface-writableby-403}
676
699
  }
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")
700
+ // {§capability-admission} — one resolver gates every operation route before
701
+ // execution or proposal handling. Unknown routes continue to their ordinary
702
+ // owner; a known policy denial is one factual, non-presumptuous 403.
703
+ async #checkCapabilities(statement, workspaceId, loopId, functionalityWorkerId) {
704
+ const denied = await this.#capabilities.denial(statement, workspaceId, functionalityWorkerId, loopId);
705
+ if (denied === null)
691
706
  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);
707
+ const { descriptor, scope } = denied;
708
+ const route = [
709
+ descriptor.operation,
710
+ descriptor.scheme,
711
+ descriptor.runtime,
712
+ descriptor.tool,
713
+ ].filter((part) => part !== undefined).join("/");
714
+ return _a.#failure("capability-denied", 403, `Capability '${route}' is denied by ${scope} policy.`, {}, {
715
+ ...descriptor,
716
+ policyScope: scope,
717
+ retryable: false,
718
+ });
763
719
  }
764
720
  // Worker control is FORK/WORK (grammar 0.74.55), not COPY — its body
765
721
  // is the new worker's seed prompt, not a destination path. The COPY gates and ResourceMutations.handleCopy
@@ -793,10 +749,15 @@ class Dispatcher {
793
749
  if (denied !== null)
794
750
  return denied;
795
751
  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);
752
+ // {§worker-delegation-inherits-policy} — authority flows down the
753
+ // delegation edge. The child receives the delegator's complete loop
754
+ // policy and an immutable snapshot of its effective capability bound.
755
+ const policy = await LoopPolicyReader.read(this.#db, ctx.loopId);
756
+ const capabilityBound = await CapabilityPolicies.delegationBound(this.#db, ctx.workspaceId, ctx.workerId, policy);
757
+ const delegationPolicy = {
758
+ ...policy,
759
+ capabilities: capabilityBound,
760
+ };
800
761
  // A name is frozen per worker but reclaimable across time ({§machine-processes-worker-origin}): a LIVE
801
762
  // sister holding it is a 409 (legible, never a raw UNIQUE 500); a free/terminated name reclaims.
802
763
  const live = await this.#db.worker_live_by_name.get({ workspace_id: ctx.workspaceId, name });
@@ -804,6 +765,16 @@ class Dispatcher {
804
765
  return _a.#failure("worker-already-running", 409, `Worker '${name}' is already running.`, {}, { worker: name, retryable: false });
805
766
  }
806
767
  if (typeof statement.signal === "string") {
768
+ // {§branch-delegation-disabled} — off the model-facing surface until it is specified and
769
+ // witnessed end to end (#396); the batches stay behind the operator knob.
770
+ if (process.env.PLURNK_SERVICE_BRANCH_DELEGATION !== "1") {
771
+ return _a.#failure("branch-delegation-disabled", 501, `${statement.op} takes no signal; branch delegation is not offered.`, {}, {
772
+ worker: name,
773
+ signal: statement.signal,
774
+ recovery: `Spawn without a signal: \`## ${statement.op}0 (worker://${name})\` with the prompt as the body.`,
775
+ retryable: false,
776
+ });
777
+ }
807
778
  if (this.#branchWorker === undefined)
808
779
  throw new Error("branch worker control: branchWorker capability absent");
809
780
  const child = await this.#branchWorker({
@@ -815,7 +786,7 @@ class Dispatcher {
815
786
  name,
816
787
  branch: statement.signal,
817
788
  prompt,
818
- flags,
789
+ policy: delegationPolicy,
819
790
  origin: ctx.writer,
820
791
  });
821
792
  return {
@@ -826,13 +797,13 @@ class Dispatcher {
826
797
  }
827
798
  if (statement.op === "FORK") {
828
799
  // 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));
800
+ const branchWorkerId = await Fork.fork(this.#db, ctx.workerId, name, capabilityBound, (scheme) => this.#schemes.entryInheritanceForStoredScheme(scheme, ctx.workerId));
830
801
  await ctx.injectWorker({
831
802
  workspaceId: ctx.workspaceId,
832
803
  workerId: branchWorkerId,
833
804
  sourceWorkerId: ctx.workerId,
834
805
  prompt,
835
- flags,
806
+ freshLoopPolicy: delegationPolicy,
836
807
  parentLoopId: ctx.loopId,
837
808
  });
838
809
  return { status: 200, body: name };
@@ -841,6 +812,7 @@ class Dispatcher {
841
812
  const row = await this.#db.fork_insert_worker.get({
842
813
  workspace_id: ctx.workspaceId, name, parent_worker_id: ctx.workerId, origin: ctx.writer,
843
814
  fork_snapshot: 0,
815
+ capability_bound: JSON.stringify(capabilityBound),
844
816
  });
845
817
  if (row === undefined)
846
818
  throw new Error("worker spawn: worker insert returned no row");
@@ -849,7 +821,7 @@ class Dispatcher {
849
821
  workerId: row.id,
850
822
  sourceWorkerId: ctx.workerId,
851
823
  prompt,
852
- flags,
824
+ freshLoopPolicy: delegationPolicy,
853
825
  parentLoopId: ctx.loopId,
854
826
  });
855
827
  return { status: 200, body: name };
@@ -869,8 +841,8 @@ class Dispatcher {
869
841
  }
870
842
  const manifest = this.#schemes.manifestFor(schemeName, ctx.functionalityWorkerId);
871
843
  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}
844
+ // log:/// KILL has already gone through the projection-curation owner.
845
+ // This path owns scheme-specific world and process KILL semantics.
874
846
  // Process-KILL: any scheme whose handler exposes kill() aborts a live stream — the
875
847
  // exec handler, registered as "exec" + under every runtime tag (sh/node), so a tag-
876
848
  // addressed stream (sh:///l/t/s) routes here, not to deleteEntry. {§exec}
@@ -997,11 +969,10 @@ class Dispatcher {
997
969
  }, this.#weighContent),
998
970
  state: "resolved", outcome: null,
999
971
  attrs: JSON.stringify(durableAttrs),
972
+ initial_folded: LogVisibility.serialize(folded ? LogVisibility.FOLDED : LogVisibility.OPEN),
1000
973
  });
1001
974
  if (row === undefined)
1002
975
  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
976
  return row.id;
1006
977
  }
1007
978
  // {§turn-ops-entry} — preserve exact admitted source beside, never instead
@@ -1026,7 +997,7 @@ class Dispatcher {
1026
997
  },
1027
998
  });
1028
999
  }
1029
- // PLAN — the model's complete current Plan. An ordinary op: dispatched like any
1000
+ // PLAN — one installment of the model's running work journal. An ordinary op: dispatched like any
1030
1001
  // other, logged, and broadcast to the client as a log entry — but a pure no-op for
1031
1002
  // state (PLAN ∉ MUTATING_OPS); its body serializes into the log row's tx, no effect.
1032
1003
  #handlePlan(statement) {
@@ -1047,6 +1018,7 @@ class Dispatcher {
1047
1018
  modelCallId,
1048
1019
  });
1049
1020
  context.onDispatch?.(logEntryId);
1021
+ await context.onSettled?.(logEntryId);
1050
1022
  return result;
1051
1023
  }
1052
1024
  // {§send-premature-terminate} — the unified PENDING SET, judged at the terminal's OWN dispatch
@@ -1061,23 +1033,20 @@ class Dispatcher {
1061
1033
  const openSubs = await this.#db.find_open_subscriptions_for_worker.all({ worker_id: workerId });
1062
1034
  const execHandler = this.#schemes.get("exec");
1063
1035
  if (openSubs.length > 0 || execHandler?.hasActiveSpawns?.(workerId) === true)
1064
- pending.push("surviving streams");
1036
+ pending.push("streams");
1065
1037
  const liveChild = await this.#db.engine_worker_has_live_child.get({ worker_id: workerId });
1066
1038
  if (liveChild !== undefined)
1067
- pending.push("surviving workers");
1039
+ pending.push("workers");
1068
1040
  const boundaries = await this.#nextPacketBoundaries(workerId, turnId);
1069
1041
  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)");
1042
+ pending.push("receipts");
1071
1043
  // A stream that closed successfully is banked, not pending: concluding on its own
1072
1044
  // success is legitimate and its output stays in the Log. A failed close is an unseen
1073
1045
  // 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("; ")}`);
1046
+ if (boundaries.streamTerminations.some(({ closeStatus }) => closeStatus >= 400))
1047
+ pending.push("failed-stream-results");
1079
1048
  if (boundaries.childTerminations)
1080
- pending.push("worker results that arrived during this turn (they land NEXT turn)");
1049
+ pending.push("worker-results");
1081
1050
  return pending;
1082
1051
  }
1083
1052
  // Results cross an observation boundary only when they have appeared in a packet;
@@ -1109,16 +1078,15 @@ class Dispatcher {
1109
1078
  return failedRows.length;
1110
1079
  }
1111
1080
  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.`, {}, {
1081
+ return _a.#failure("unobserved-failures", 409, `This turn produced ${failCount} failed operation result(s) that have not yet entered a packet.`, {}, {
1113
1082
  failures: failCount,
1114
1083
  stage: "completion",
1115
- recovery: "Review the failed log items before concluding.",
1116
1084
  retryable: false,
1117
1085
  });
1118
1086
  }
1119
1087
  // J — a live obligation to WAIT on: a spawned child or an open stream (NOT retrievals, which land
1120
1088
  // next turn regardless). The wait-side twin of #pendingSet's stream+child legs ({§wait-obligation-matrix}).
1121
- async #hasLiveWork(workerId) {
1089
+ async hasLiveWork(workerId) {
1122
1090
  const openSubs = await this.#db.find_open_subscriptions_for_worker.all({ worker_id: workerId });
1123
1091
  if (openSubs.length > 0)
1124
1092
  return true;
@@ -1165,8 +1133,9 @@ class Dispatcher {
1165
1133
  // retrieval (R) just lands next turn, so the wait continues.
1166
1134
  if (status === 202) {
1167
1135
  const marks = statement.lineMarker?.marks[0];
1168
- const seconds = typeof marks === "number" ? marks : -1; // bare 202 / absent T = indefinite, bounded by the join
1169
- if (await this.#hasLiveWork(workerId)) {
1136
+ // `<T>` is MINUTES, held in seconds; bare 202 / absent T = indefinite, bounded by the join.
1137
+ const seconds = typeof marks === "number" ? (marks > 0 ? marks * 60 : marks) : -1;
1138
+ if (await this.hasLiveWork(workerId)) {
1170
1139
  if (!await this.#lifecycle.park(loopId)) {
1171
1140
  return _a.#statusResult(await this.#lifecycle.status(loopId), "loop-already-terminal", "The loop was already terminal when SEND attempted to wait.");
1172
1141
  }
@@ -1214,19 +1183,17 @@ class Dispatcher {
1214
1183
  if (pending.length > 0) {
1215
1184
  // A receipts-only refusal needs no KILL/park remedy menu: the results simply
1216
1185
  // arrive in the next packet. Streams and children retain their remedy steer.
1217
- const receiptsOnly = pending.every((k) => k.startsWith("this turn's receipts"));
1186
+ const receiptsOnly = pending.every((kind) => kind === "receipts");
1218
1187
  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.", {}, {
1188
+ return _a.#failure("retrieval-results-unobserved", 409, "Completion preceded this turn's operation results; they enter the next packet.", {}, {
1220
1189
  pending: [...pending],
1221
1190
  stage: "completion",
1222
- recovery: "Review the results, then use only `# PLAN0` and `## SEND0 [200]` to conclude.",
1223
1191
  retryable: false,
1224
1192
  });
1225
1193
  }
1226
- return _a.#failure("work-remains", 409, `Completion was refused while work remains: ${pending.join("; ")}.`, {}, {
1194
+ return _a.#failure("work-remains", 409, "Completion encountered pending work or results.", {}, {
1227
1195
  pending: [...pending],
1228
1196
  stage: "completion",
1229
- recovery: "Resolve the listed pending work before concluding.",
1230
1197
  retryable: false,
1231
1198
  });
1232
1199
  }
@@ -1241,7 +1208,11 @@ class Dispatcher {
1241
1208
  const branchDenial = await this.#branchCompletionGate?.(workerId) ?? null;
1242
1209
  if (branchDenial !== null)
1243
1210
  return branchDenial;
1244
- const failure = _a.#failure("scope-abandoned", 499, raw === "" ? "The worker abandoned its scope." : raw);
1211
+ const reason = raw === "" ? null : ErrorDetail.preview(raw);
1212
+ const failure = _a.#failure("scope-abandoned", 499, "The worker ended its scope with SEND[499].", {}, {
1213
+ ...(reason === null ? {} : { reason }),
1214
+ retryable: false,
1215
+ });
1245
1216
  const seqs = await this.#db.engine_loop_turn_seqs.get({
1246
1217
  loop_id: loopId,
1247
1218
  turn_id: turnId,
@@ -1253,7 +1224,7 @@ class Dispatcher {
1253
1224
  const finished = await this.#lifecycle.finish(loopId, failure);
1254
1225
  if (finished === null)
1255
1226
  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);
1227
+ await this.#cancelDescendants?.(workerId, reason ?? "parent worker ended its scope with SEND[499]");
1257
1228
  return failure;
1258
1229
  }
1259
1230
  // Every other signal — 102 bare, 202 (retired as a terminal; now ordinary mid-comms), 1xx —
@@ -1287,6 +1258,27 @@ class Dispatcher {
1287
1258
  const addressedScheme = statement.target?.kind === "url" ? statement.target.scheme : null;
1288
1259
  if (manifest === undefined)
1289
1260
  throw new Error(`scheme '${schemeName}' has no manifest`);
1261
+ // Metadata belongs to the handler that receives it. EXEC over a
1262
+ // non-file resource is the one split route: exec hosts the operation,
1263
+ // while the canonically routed source scheme receives the metadata.
1264
+ const metadataScheme = schemeName === "exec"
1265
+ && statement.target?.kind === "url"
1266
+ && statement.target.scheme !== "file"
1267
+ ? schemeNameOf(statement.target) ?? schemeName
1268
+ : schemeName;
1269
+ const metadataManifest = metadataScheme === schemeName
1270
+ ? manifest
1271
+ : this.#schemes.manifestFor(metadataScheme, ctx.functionalityWorkerId);
1272
+ if (statement.metadata !== null && metadataManifest === undefined) {
1273
+ return _a.#failure("scheme-not-found", 501, `Scheme '${metadataScheme}' is not registered.`, {}, { scheme: metadataScheme, retryable: false });
1274
+ }
1275
+ if (statement.metadata !== null && metadataManifest?.metadataModifier !== true) {
1276
+ return _a.#failure("scheme-metadata-unsupported", 400, `Scheme '${metadataScheme}' does not accept the {metadata} modifier.`, {}, {
1277
+ scheme: metadataScheme,
1278
+ operation: statement.op,
1279
+ retryable: false,
1280
+ });
1281
+ }
1290
1282
  const publishedChannel = statement.target?.kind === "url"
1291
1283
  ? statement.target.fragment ?? manifest.defaultChannel
1292
1284
  : manifest.defaultChannel;
@@ -1379,6 +1371,7 @@ class Dispatcher {
1379
1371
  ? { address: null, result: null }
1380
1372
  : await this.#prepareDataRepresentation({
1381
1373
  target: statement.target,
1374
+ metadata: statement.metadata,
1382
1375
  routedScheme: schemeName,
1383
1376
  handler: handler,
1384
1377
  manifest,
@@ -1412,10 +1405,6 @@ class Dispatcher {
1412
1405
  return _a.#failure("operation-not-implemented", 501, `Scheme '${schemeName}' does not implement ${statement.op}.`, {}, {
1413
1406
  scheme: schemeName,
1414
1407
  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
1408
  retryable: false,
1420
1409
  });
1421
1410
  }
@@ -1430,6 +1419,7 @@ class Dispatcher {
1430
1419
  if (exactTarget && statement.target !== null) {
1431
1420
  const prepared = await this.#prepareDataRepresentation({
1432
1421
  target: statement.target,
1422
+ metadata: statement.metadata,
1433
1423
  routedScheme: schemeName,
1434
1424
  handler: handler,
1435
1425
  manifest,
@@ -1504,10 +1494,11 @@ class Dispatcher {
1504
1494
  }
1505
1495
  async #writeLog({ statement, result, workspaceId, workerId, functionalityWorkerId, loopId, turnId, sequence, origin, curationPlan, modelCallId, }) {
1506
1496
  const durableStatement = DurableStatement.project(statement);
1507
- const target = this.#extractTarget(durableStatement.target, functionalityWorkerId);
1497
+ const target = this.#extractTarget(primaryTargetOf(durableStatement), functionalityWorkerId);
1508
1498
  await this.#canonColumns(target, workspaceId); // {§fs-answer-in-canon}
1509
- const lineMarkerJson = "lineMarker" in durableStatement && durableStatement.lineMarker !== null
1510
- ? JSON.stringify(durableStatement.lineMarker)
1499
+ const lineMarker = primaryLineMarkerOf(durableStatement);
1500
+ const lineMarkerJson = lineMarker !== null
1501
+ ? JSON.stringify(lineMarker)
1511
1502
  : null;
1512
1503
  // A proposal (status 202 from a side-effecting op) is written to the log in
1513
1504
  // state='proposed' until the proposal lifecycle resolves it; attrs holds the
@@ -1607,6 +1598,7 @@ class Dispatcher {
1607
1598
  state: isProposed ? "proposed" : "resolved",
1608
1599
  outcome: null,
1609
1600
  attrs,
1601
+ initial_folded: LogVisibility.serialize(LogVisibility.OPEN),
1610
1602
  });
1611
1603
  // {§exec-tag-signal} — an EXEC row's signal is its runtime; its tag signal classifies
1612
1604
  // through the same idempotent primitive the log-write trigger uses for tag operations.