@plurnk/plurnk-service 1.3.12 → 1.4.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 (469) hide show
  1. package/.env.defaults +84 -77
  2. package/INSTALL.md +50 -25
  3. package/README.md +24 -10
  4. package/SPEC.md +2311 -928
  5. package/dist/Paths.d.ts +4 -1
  6. package/dist/Paths.d.ts.map +1 -1
  7. package/dist/Paths.js +23 -21
  8. package/dist/Paths.js.map +1 -1
  9. package/dist/build-info.json +1 -1
  10. package/dist/content/edit-receipt.d.ts +13 -28
  11. package/dist/content/edit-receipt.d.ts.map +1 -1
  12. package/dist/content/edit-receipt.js +285 -104
  13. package/dist/content/edit-receipt.js.map +1 -1
  14. package/dist/content/edited-span.d.ts.map +1 -1
  15. package/dist/content/edited-span.js +2 -5
  16. package/dist/content/edited-span.js.map +1 -1
  17. package/dist/content/index.d.ts +3 -3
  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-marker.d.ts +6 -9
  22. package/dist/content/line-marker.d.ts.map +1 -1
  23. package/dist/content/line-marker.js +7 -11
  24. package/dist/content/line-marker.js.map +1 -1
  25. package/dist/content/matcher.d.ts +14 -25
  26. package/dist/content/matcher.d.ts.map +1 -1
  27. package/dist/content/matcher.js +77 -107
  28. package/dist/content/matcher.js.map +1 -1
  29. package/dist/content/mimetype-binary.d.ts +2 -2
  30. package/dist/content/mimetype-binary.d.ts.map +1 -1
  31. package/dist/content/mimetype-binary.js +10 -10
  32. package/dist/content/mimetype-binary.js.map +1 -1
  33. package/dist/content/path-mimetype.js +3 -3
  34. package/dist/content/path-mimetype.js.map +1 -1
  35. package/dist/content/read-resolve.d.ts +8 -4
  36. package/dist/content/read-resolve.d.ts.map +1 -1
  37. package/dist/content/read-resolve.js +101 -49
  38. package/dist/content/read-resolve.js.map +1 -1
  39. package/dist/core/BranchReceipt.d.ts +7 -0
  40. package/dist/core/BranchReceipt.d.ts.map +1 -0
  41. package/dist/core/BranchReceipt.js +27 -0
  42. package/dist/core/BranchReceipt.js.map +1 -0
  43. package/dist/core/BudgetOverflow.d.ts +23 -0
  44. package/dist/core/BudgetOverflow.d.ts.map +1 -0
  45. package/dist/core/BudgetOverflow.js +61 -0
  46. package/dist/core/BudgetOverflow.js.map +1 -0
  47. package/dist/core/BudgetReadout.d.ts +8 -0
  48. package/dist/core/BudgetReadout.d.ts.map +1 -0
  49. package/dist/core/BudgetReadout.js +79 -0
  50. package/dist/core/BudgetReadout.js.map +1 -0
  51. package/dist/core/ChannelWrite.d.ts +28 -9
  52. package/dist/core/ChannelWrite.d.ts.map +1 -1
  53. package/dist/core/ChannelWrite.js +28 -21
  54. package/dist/core/ChannelWrite.js.map +1 -1
  55. package/dist/core/ChannelWrite.sql +11 -10
  56. package/dist/core/CoreSchemeServices.d.ts +10 -4
  57. package/dist/core/CoreSchemeServices.d.ts.map +1 -1
  58. package/dist/core/CoreSchemeServices.js +7 -1
  59. package/dist/core/CoreSchemeServices.js.map +1 -1
  60. package/dist/core/Dispatcher.d.ts +29 -20
  61. package/dist/core/Dispatcher.d.ts.map +1 -1
  62. package/dist/core/Dispatcher.js +1488 -599
  63. package/dist/core/Dispatcher.js.map +1 -1
  64. package/dist/core/DurableStatement.d.ts +6 -0
  65. package/dist/core/DurableStatement.d.ts.map +1 -0
  66. package/dist/core/DurableStatement.js +51 -0
  67. package/dist/core/DurableStatement.js.map +1 -0
  68. package/dist/core/Engine.d.ts +48 -20
  69. package/dist/core/Engine.d.ts.map +1 -1
  70. package/dist/core/Engine.js +1208 -789
  71. package/dist/core/Engine.js.map +1 -1
  72. package/dist/core/Engine.sql +309 -217
  73. package/dist/core/EnvFlags.js +1 -1
  74. package/dist/core/EnvFlags.js.map +1 -1
  75. package/dist/core/ErrorDetail.d.ts +6 -0
  76. package/dist/core/ErrorDetail.d.ts.map +1 -0
  77. package/dist/core/ErrorDetail.js +20 -0
  78. package/dist/core/ErrorDetail.js.map +1 -0
  79. package/dist/core/ExecutorRegistry.d.ts +14 -3
  80. package/dist/core/ExecutorRegistry.d.ts.map +1 -1
  81. package/dist/core/ExecutorRegistry.js +56 -30
  82. package/dist/core/ExecutorRegistry.js.map +1 -1
  83. package/dist/core/GitBranch.d.ts +20 -0
  84. package/dist/core/GitBranch.d.ts.map +1 -0
  85. package/dist/core/GitBranch.js +110 -0
  86. package/dist/core/GitBranch.js.map +1 -0
  87. package/dist/core/JournalTurn.d.ts +9 -0
  88. package/dist/core/JournalTurn.d.ts.map +1 -0
  89. package/dist/core/JournalTurn.js +14 -0
  90. package/dist/core/JournalTurn.js.map +1 -0
  91. package/dist/core/JournalTurn.sql +10 -0
  92. package/dist/core/LogBody.d.ts +17 -0
  93. package/dist/core/LogBody.d.ts.map +1 -0
  94. package/dist/core/LogBody.js +148 -0
  95. package/dist/core/LogBody.js.map +1 -0
  96. package/dist/core/LoopFlagsReader.d.ts +7 -0
  97. package/dist/core/LoopFlagsReader.d.ts.map +1 -0
  98. package/dist/core/LoopFlagsReader.js +33 -0
  99. package/dist/core/LoopFlagsReader.js.map +1 -0
  100. package/dist/core/LoopLifecycle.d.ts +8 -1
  101. package/dist/core/LoopLifecycle.d.ts.map +1 -1
  102. package/dist/core/LoopLifecycle.js +55 -9
  103. package/dist/core/LoopLifecycle.js.map +1 -1
  104. package/dist/core/LoopLifecycle.sql +9 -3
  105. package/dist/core/NoticeChannel.d.ts +13 -0
  106. package/dist/core/NoticeChannel.d.ts.map +1 -0
  107. package/dist/core/NoticeChannel.js +40 -0
  108. package/dist/core/NoticeChannel.js.map +1 -0
  109. package/dist/core/Owner.d.ts +0 -1
  110. package/dist/core/Owner.d.ts.map +1 -1
  111. package/dist/core/Owner.js +2 -5
  112. package/dist/core/Owner.js.map +1 -1
  113. package/dist/core/PacketBuilder.d.ts +25 -24
  114. package/dist/core/PacketBuilder.d.ts.map +1 -1
  115. package/dist/core/PacketBuilder.js +313 -375
  116. package/dist/core/PacketBuilder.js.map +1 -1
  117. package/dist/core/ProblemLog.d.ts +26 -0
  118. package/dist/core/ProblemLog.d.ts.map +1 -0
  119. package/dist/core/ProblemLog.js +58 -0
  120. package/dist/core/ProblemLog.js.map +1 -0
  121. package/dist/core/ProposalLifecycle.d.ts +15 -21
  122. package/dist/core/ProposalLifecycle.d.ts.map +1 -1
  123. package/dist/core/ProposalLifecycle.js +291 -78
  124. package/dist/core/ProposalLifecycle.js.map +1 -1
  125. package/dist/core/ProviderInstantiate.d.ts.map +1 -1
  126. package/dist/core/ProviderInstantiate.js +51 -48
  127. package/dist/core/ProviderInstantiate.js.map +1 -1
  128. package/dist/core/SchemeRegistry.d.ts +14 -8
  129. package/dist/core/SchemeRegistry.d.ts.map +1 -1
  130. package/dist/core/SchemeRegistry.js +189 -114
  131. package/dist/core/SchemeRegistry.js.map +1 -1
  132. package/dist/core/ServiceTeardown.d.ts +11 -0
  133. package/dist/core/ServiceTeardown.d.ts.map +1 -0
  134. package/dist/core/ServiceTeardown.js +70 -0
  135. package/dist/core/ServiceTeardown.js.map +1 -0
  136. package/dist/core/StoredPacket.d.ts +29 -0
  137. package/dist/core/StoredPacket.d.ts.map +1 -0
  138. package/dist/core/StoredPacket.js +147 -0
  139. package/dist/core/StoredPacket.js.map +1 -0
  140. package/dist/core/StrikeRail.d.ts +1 -2
  141. package/dist/core/StrikeRail.d.ts.map +1 -1
  142. package/dist/core/StrikeRail.js +25 -55
  143. package/dist/core/StrikeRail.js.map +1 -1
  144. package/dist/core/WorkerControlAddress.d.ts +17 -0
  145. package/dist/core/WorkerControlAddress.d.ts.map +1 -0
  146. package/dist/core/WorkerControlAddress.js +44 -0
  147. package/dist/core/WorkerControlAddress.js.map +1 -0
  148. package/dist/core/WorkerName.d.ts +31 -0
  149. package/dist/core/WorkerName.d.ts.map +1 -0
  150. package/dist/core/WorkerName.js +95 -0
  151. package/dist/core/WorkerName.js.map +1 -0
  152. package/dist/core/WorkerName.sql +36 -0
  153. package/dist/core/WorkspaceGate.d.ts +14 -0
  154. package/dist/core/WorkspaceGate.d.ts.map +1 -0
  155. package/dist/core/WorkspaceGate.js +155 -0
  156. package/dist/core/WorkspaceGate.js.map +1 -0
  157. package/dist/core/caps/CapsResolve.d.ts +1 -1
  158. package/dist/core/caps/CapsResolve.d.ts.map +1 -1
  159. package/dist/core/caps/CapsResolve.js.map +1 -1
  160. package/dist/core/caps/DbChannelCaps.d.ts +5 -11
  161. package/dist/core/caps/DbChannelCaps.d.ts.map +1 -1
  162. package/dist/core/caps/DbChannelCaps.js +24 -8
  163. package/dist/core/caps/DbChannelCaps.js.map +1 -1
  164. package/dist/core/caps/DbEntryCaps.d.ts +4 -13
  165. package/dist/core/caps/DbEntryCaps.d.ts.map +1 -1
  166. package/dist/core/caps/DbEntryCaps.js +15 -6
  167. package/dist/core/caps/DbEntryCaps.js.map +1 -1
  168. package/dist/core/caps/DbNotifyCaps.d.ts +1 -1
  169. package/dist/core/caps/DbNotifyCaps.d.ts.map +1 -1
  170. package/dist/core/caps/DbNotifyCaps.js +12 -11
  171. package/dist/core/caps/DbNotifyCaps.js.map +1 -1
  172. package/dist/core/caps/DbProjectionCaps.d.ts +5 -5
  173. package/dist/core/caps/DbProjectionCaps.d.ts.map +1 -1
  174. package/dist/core/caps/DbProjectionCaps.js +21 -4
  175. package/dist/core/caps/DbProjectionCaps.js.map +1 -1
  176. package/dist/core/caps/DbSubscriptionCaps.d.ts +4 -4
  177. package/dist/core/caps/DbSubscriptionCaps.d.ts.map +1 -1
  178. package/dist/core/caps/DbSubscriptionCaps.js +57 -62
  179. package/dist/core/caps/DbSubscriptionCaps.js.map +1 -1
  180. package/dist/core/caps/DbTagCaps.d.ts +5 -12
  181. package/dist/core/caps/DbTagCaps.d.ts.map +1 -1
  182. package/dist/core/caps/DbTagCaps.js +14 -7
  183. package/dist/core/caps/DbTagCaps.js.map +1 -1
  184. package/dist/core/content-hash.js +1 -1
  185. package/dist/core/content-hash.js.map +1 -1
  186. package/dist/core/env-defaults.d.ts.map +1 -1
  187. package/dist/core/env-defaults.js.map +1 -1
  188. package/dist/core/fork.d.ts.map +1 -1
  189. package/dist/core/fork.js +55 -30
  190. package/dist/core/fork.js.map +1 -1
  191. package/dist/core/fork.sql +41 -27
  192. package/dist/core/git-env.d.ts +2 -0
  193. package/dist/core/git-env.d.ts.map +1 -1
  194. package/dist/core/git-env.js +13 -18
  195. package/dist/core/git-env.js.map +1 -1
  196. package/dist/core/git-iso.d.ts.map +1 -1
  197. package/dist/core/git-iso.js +9 -15
  198. package/dist/core/git-iso.js.map +1 -1
  199. package/dist/core/git-membership.d.ts +1 -1
  200. package/dist/core/git-membership.d.ts.map +1 -1
  201. package/dist/core/git-membership.js +198 -146
  202. package/dist/core/git-membership.js.map +1 -1
  203. package/dist/core/git-state.d.ts.map +1 -1
  204. package/dist/core/git-state.js +10 -11
  205. package/dist/core/git-state.js.map +1 -1
  206. package/dist/core/owner.sql +1 -1
  207. package/dist/core/packet-inject.d.ts.map +1 -1
  208. package/dist/core/packet-inject.js +7 -11
  209. package/dist/core/packet-inject.js.map +1 -1
  210. package/dist/core/packet-wire.d.ts +2 -20
  211. package/dist/core/packet-wire.d.ts.map +1 -1
  212. package/dist/core/packet-wire.js +242 -368
  213. package/dist/core/packet-wire.js.map +1 -1
  214. package/dist/core/plurnk-uri.d.ts +12 -1
  215. package/dist/core/plurnk-uri.d.ts.map +1 -1
  216. package/dist/core/plurnk-uri.js +49 -29
  217. package/dist/core/plurnk-uri.js.map +1 -1
  218. package/dist/core/results.d.ts +16 -5
  219. package/dist/core/results.d.ts.map +1 -1
  220. package/dist/core/results.js +27 -18
  221. package/dist/core/results.js.map +1 -1
  222. package/dist/core/ruler_count.d.ts +4 -0
  223. package/dist/core/ruler_count.d.ts.map +1 -0
  224. package/dist/core/ruler_count.js +7 -0
  225. package/dist/core/ruler_count.js.map +1 -0
  226. package/dist/core/scheme-types.d.ts +9 -11
  227. package/dist/core/scheme-types.d.ts.map +1 -1
  228. package/dist/core/scheme-types.js.map +1 -1
  229. package/dist/core/search-gate.d.ts.map +1 -1
  230. package/dist/core/search-gate.js +3 -11
  231. package/dist/core/search-gate.js.map +1 -1
  232. package/dist/core/teaching-corpus.d.ts +6 -0
  233. package/dist/core/teaching-corpus.d.ts.map +1 -0
  234. package/dist/core/teaching-corpus.js +23 -0
  235. package/dist/core/teaching-corpus.js.map +1 -0
  236. package/dist/core/teaching.d.ts +0 -1
  237. package/dist/core/teaching.d.ts.map +1 -1
  238. package/dist/core/teaching.js +2 -8
  239. package/dist/core/teaching.js.map +1 -1
  240. package/dist/core/token-ruler.d.ts.map +1 -1
  241. package/dist/core/token-ruler.js +3 -16
  242. package/dist/core/token-ruler.js.map +1 -1
  243. package/dist/core/turn-scheduler.d.ts +1 -1
  244. package/dist/core/turn-scheduler.d.ts.map +1 -1
  245. package/dist/core/turn-scheduler.js +1 -1
  246. package/dist/core/turn-scheduler.js.map +1 -1
  247. package/dist/core/worker-cap.d.ts +2 -4
  248. package/dist/core/worker-cap.d.ts.map +1 -1
  249. package/dist/core/worker-cap.js +13 -4
  250. package/dist/core/worker-cap.js.map +1 -1
  251. package/dist/core/{run-ops.sql → worker-ops.sql} +11 -11
  252. package/dist/core/workspace-settings.d.ts.map +1 -1
  253. package/dist/core/workspace-settings.js +17 -9
  254. package/dist/core/workspace-settings.js.map +1 -1
  255. package/dist/digest/Digest.d.ts +2 -1
  256. package/dist/digest/Digest.d.ts.map +1 -1
  257. package/dist/digest/Digest.js +337 -138
  258. package/dist/digest/Digest.js.map +1 -1
  259. package/dist/digest/digest.sql +19 -10
  260. package/dist/index.d.ts.map +1 -1
  261. package/dist/index.js +3 -5
  262. package/dist/index.js.map +1 -1
  263. package/dist/matrix/live-specimen.d.ts +9 -0
  264. package/dist/matrix/live-specimen.d.ts.map +1 -0
  265. package/dist/matrix/live-specimen.js +37 -0
  266. package/dist/matrix/live-specimen.js.map +1 -0
  267. package/dist/observe/api.d.ts +6 -0
  268. package/dist/observe/api.d.ts.map +1 -0
  269. package/dist/observe/api.js +11 -0
  270. package/dist/observe/api.js.map +1 -0
  271. package/dist/observe/init.d.ts +7 -0
  272. package/dist/observe/init.d.ts.map +1 -0
  273. package/dist/observe/init.js +159 -0
  274. package/dist/observe/init.js.map +1 -0
  275. package/dist/observe/metrics.d.ts +6 -0
  276. package/dist/observe/metrics.d.ts.map +1 -0
  277. package/dist/observe/metrics.js +15 -0
  278. package/dist/observe/metrics.js.map +1 -0
  279. package/dist/observe/spans.d.ts +4 -0
  280. package/dist/observe/spans.d.ts.map +1 -0
  281. package/dist/observe/spans.js +51 -0
  282. package/dist/observe/spans.js.map +1 -0
  283. package/dist/schemes/EffectPolicy.d.ts +1 -0
  284. package/dist/schemes/EffectPolicy.d.ts.map +1 -1
  285. package/dist/schemes/EffectPolicy.js +5 -2
  286. package/dist/schemes/EffectPolicy.js.map +1 -1
  287. package/dist/schemes/Exec.d.ts +6 -11
  288. package/dist/schemes/Exec.d.ts.map +1 -1
  289. package/dist/schemes/Exec.js +364 -247
  290. package/dist/schemes/Exec.js.map +1 -1
  291. package/dist/schemes/ExecOutputScheme.d.ts +8 -7
  292. package/dist/schemes/ExecOutputScheme.d.ts.map +1 -1
  293. package/dist/schemes/ExecOutputScheme.js +48 -9
  294. package/dist/schemes/ExecOutputScheme.js.map +1 -1
  295. package/dist/schemes/File.d.ts +7 -11
  296. package/dist/schemes/File.d.ts.map +1 -1
  297. package/dist/schemes/File.js +279 -142
  298. package/dist/schemes/File.js.map +1 -1
  299. package/dist/schemes/Log.d.ts +4 -8
  300. package/dist/schemes/Log.d.ts.map +1 -1
  301. package/dist/schemes/Log.js +475 -208
  302. package/dist/schemes/Log.js.map +1 -1
  303. package/dist/schemes/Log.sql +62 -30
  304. package/dist/schemes/Prompt.d.ts +3 -2
  305. package/dist/schemes/Prompt.d.ts.map +1 -1
  306. package/dist/schemes/Prompt.js +9 -6
  307. package/dist/schemes/Prompt.js.map +1 -1
  308. package/dist/schemes/Skill.d.ts +1 -1
  309. package/dist/schemes/Skill.d.ts.map +1 -1
  310. package/dist/schemes/Skill.js +2 -3
  311. package/dist/schemes/Skill.js.map +1 -1
  312. package/dist/schemes/Worker.d.ts +8 -16
  313. package/dist/schemes/Worker.d.ts.map +1 -1
  314. package/dist/schemes/Worker.js +179 -69
  315. package/dist/schemes/Worker.js.map +1 -1
  316. package/dist/schemes/_entry-chunk.d.ts.map +1 -1
  317. package/dist/schemes/_entry-chunk.js +12 -5
  318. package/dist/schemes/_entry-chunk.js.map +1 -1
  319. package/dist/schemes/_entry-crud.d.ts +10 -9
  320. package/dist/schemes/_entry-crud.d.ts.map +1 -1
  321. package/dist/schemes/_entry-crud.js +59 -13
  322. package/dist/schemes/_entry-crud.js.map +1 -1
  323. package/dist/schemes/_entry-crud.sql +20 -8
  324. package/dist/schemes/_entry-find.d.ts +20 -15
  325. package/dist/schemes/_entry-find.d.ts.map +1 -1
  326. package/dist/schemes/_entry-find.js +339 -174
  327. package/dist/schemes/_entry-find.js.map +1 -1
  328. package/dist/schemes/_entry-find.sql +20 -8
  329. package/dist/schemes/_entry-graph.d.ts +4 -2
  330. package/dist/schemes/_entry-graph.d.ts.map +1 -1
  331. package/dist/schemes/_entry-graph.js +55 -31
  332. package/dist/schemes/_entry-graph.js.map +1 -1
  333. package/dist/schemes/_entry-graph.sql +37 -22
  334. package/dist/schemes/_entry-manifest.d.ts +10 -15
  335. package/dist/schemes/_entry-manifest.d.ts.map +1 -1
  336. package/dist/schemes/_entry-manifest.js +50 -268
  337. package/dist/schemes/_entry-manifest.js.map +1 -1
  338. package/dist/schemes/_entry-ops.d.ts +9 -14
  339. package/dist/schemes/_entry-ops.d.ts.map +1 -1
  340. package/dist/schemes/_entry-ops.js +149 -77
  341. package/dist/schemes/_entry-ops.js.map +1 -1
  342. package/dist/schemes/_entry-ops.sql +1 -1
  343. package/dist/schemes/_entry-semantic.d.ts +23 -8
  344. package/dist/schemes/_entry-semantic.d.ts.map +1 -1
  345. package/dist/schemes/_entry-semantic.js +141 -60
  346. package/dist/schemes/_entry-semantic.js.map +1 -1
  347. package/dist/schemes/_entry-semantic.sql +63 -45
  348. package/dist/schemes/_entry-send.d.ts +3 -4
  349. package/dist/schemes/_entry-send.d.ts.map +1 -1
  350. package/dist/schemes/_entry-send.js +48 -23
  351. package/dist/schemes/_entry-send.js.map +1 -1
  352. package/dist/schemes/_path-scope.d.ts +23 -0
  353. package/dist/schemes/_path-scope.d.ts.map +1 -0
  354. package/dist/schemes/_path-scope.js +61 -0
  355. package/dist/schemes/_path-scope.js.map +1 -0
  356. package/dist/schemes/_search-candidate.d.ts +20 -0
  357. package/dist/schemes/_search-candidate.d.ts.map +1 -0
  358. package/dist/schemes/_search-candidate.js +19 -0
  359. package/dist/schemes/_search-candidate.js.map +1 -0
  360. package/dist/schemes/_search-exclusion.d.ts +7 -0
  361. package/dist/schemes/_search-exclusion.d.ts.map +1 -0
  362. package/dist/schemes/_search-exclusion.js +23 -0
  363. package/dist/schemes/_search-exclusion.js.map +1 -0
  364. package/dist/schemes/_search-index.d.ts +6 -0
  365. package/dist/schemes/_search-index.d.ts.map +1 -0
  366. package/dist/schemes/_search-index.js +326 -0
  367. package/dist/schemes/_search-index.js.map +1 -0
  368. package/dist/schemes/cosine.js +1 -1
  369. package/dist/schemes/cosine.js.map +1 -1
  370. package/dist/schemes/exec-abort.js +3 -3
  371. package/dist/schemes/exec-abort.js.map +1 -1
  372. package/dist/schemes/exec-env.js +3 -3
  373. package/dist/schemes/exec-env.js.map +1 -1
  374. package/dist/server/BranchBatches.d.ts +48 -0
  375. package/dist/server/BranchBatches.d.ts.map +1 -0
  376. package/dist/server/BranchBatches.js +591 -0
  377. package/dist/server/BranchBatches.js.map +1 -0
  378. package/dist/server/Daemon.d.ts +36 -66
  379. package/dist/server/Daemon.d.ts.map +1 -1
  380. package/dist/server/Daemon.js +920 -446
  381. package/dist/server/Daemon.js.map +1 -1
  382. package/dist/server/DaemonModule.d.ts +31 -0
  383. package/dist/server/DaemonModule.d.ts.map +1 -0
  384. package/dist/server/DaemonModule.js +2 -0
  385. package/dist/server/DaemonModule.js.map +1 -0
  386. package/dist/server/branch-batch.sql +152 -0
  387. package/dist/server/client-input.d.ts +12 -0
  388. package/dist/server/client-input.d.ts.map +1 -1
  389. package/dist/server/client-input.js +262 -57
  390. package/dist/server/client-input.js.map +1 -1
  391. package/dist/server/dispatch-as-plurnk.d.ts +1 -1
  392. package/dist/server/dispatch-as-plurnk.d.ts.map +1 -1
  393. package/dist/server/dispatch-as-plurnk.js +15 -7
  394. package/dist/server/dispatch-as-plurnk.js.map +1 -1
  395. package/dist/server/drain.sql +92 -62
  396. package/dist/server/envelope.d.ts +2 -5
  397. package/dist/server/envelope.d.ts.map +1 -1
  398. package/dist/server/envelope.js +119 -95
  399. package/dist/server/envelope.js.map +1 -1
  400. package/dist/server/envelope.sql +19 -23
  401. package/dist/server/exec-poll-backoff.js +1 -1
  402. package/dist/server/exec-poll-backoff.js.map +1 -1
  403. package/dist/server/lifecycle-recovery.sql +79 -2
  404. package/dist/server/logEntry.d.ts +3 -1
  405. package/dist/server/logEntry.d.ts.map +1 -1
  406. package/dist/server/logEntry.js +4 -2
  407. package/dist/server/logEntry.js.map +1 -1
  408. package/dist/server/logEntry.sql +1 -1
  409. package/dist/server/loop-model.d.ts.map +1 -1
  410. package/dist/server/loop-model.js +22 -9
  411. package/dist/server/loop-model.js.map +1 -1
  412. package/dist/server/loopDocs.d.ts.map +1 -1
  413. package/dist/server/loopDocs.js +5 -4
  414. package/dist/server/loopDocs.js.map +1 -1
  415. package/dist/server/seam-entry-read.sql +15 -10
  416. package/dist/server/seam-log-read.sql +2 -2
  417. package/dist/server/seam-loop.sql +1 -1
  418. package/dist/server/seam-proposal-list.sql +17 -7
  419. package/dist/service.d.ts.map +1 -1
  420. package/dist/service.js +66 -60
  421. package/dist/service.js.map +1 -1
  422. package/migrations/001_schema.sql +897 -0
  423. package/package.json +57 -26
  424. package/dist/core/PluginLoader.d.ts +0 -20
  425. package/dist/core/PluginLoader.d.ts.map +0 -1
  426. package/dist/core/PluginLoader.js +0 -142
  427. package/dist/core/PluginLoader.js.map +0 -1
  428. package/dist/core/TelemetryChannel.d.ts +0 -37
  429. package/dist/core/TelemetryChannel.d.ts.map +0 -1
  430. package/dist/core/TelemetryChannel.js +0 -80
  431. package/dist/core/TelemetryChannel.js.map +0 -1
  432. package/dist/core/path-decode.d.ts +0 -3
  433. package/dist/core/path-decode.d.ts.map +0 -1
  434. package/dist/core/path-decode.js +0 -9
  435. package/dist/core/path-decode.js.map +0 -1
  436. package/dist/core/plugin-attribution.d.ts +0 -5
  437. package/dist/core/plugin-attribution.d.ts.map +0 -1
  438. package/dist/core/plugin-attribution.js +0 -39
  439. package/dist/core/plugin-attribution.js.map +0 -1
  440. package/dist/core/world-state.d.ts +0 -10
  441. package/dist/core/world-state.d.ts.map +0 -1
  442. package/dist/core/world-state.js +0 -35
  443. package/dist/core/world-state.js.map +0 -1
  444. package/dist/core/world-state.sql +0 -35
  445. package/dist/core/zero-pin.d.ts +0 -3
  446. package/dist/core/zero-pin.d.ts.map +0 -1
  447. package/dist/core/zero-pin.js +0 -16
  448. package/dist/core/zero-pin.js.map +0 -1
  449. package/dist/server/auto.d.ts +0 -6
  450. package/dist/server/auto.d.ts.map +0 -1
  451. package/dist/server/auto.js +0 -63
  452. package/dist/server/auto.js.map +0 -1
  453. package/dist/server/clientTurn.d.ts +0 -6
  454. package/dist/server/clientTurn.d.ts.map +0 -1
  455. package/dist/server/clientTurn.js +0 -22
  456. package/dist/server/clientTurn.js.map +0 -1
  457. package/dist/server/clientTurn.sql +0 -10
  458. package/dist/server/noProposals.d.ts +0 -6
  459. package/dist/server/noProposals.d.ts.map +0 -1
  460. package/dist/server/noProposals.js +0 -37
  461. package/dist/server/noProposals.js.map +0 -1
  462. package/dist/server/version-info.d.ts +0 -14
  463. package/dist/server/version-info.d.ts.map +0 -1
  464. package/dist/server/version-info.js +0 -69
  465. package/dist/server/version-info.js.map +0 -1
  466. package/migrations/0000-00-00.01_schema.sql +0 -536
  467. package/migrations/0004_loop-provider.sql +0 -3
  468. package/migrations/0005_subscription-published-channel.sql +0 -3
  469. package/migrations/0006_loop-max-turns.sql +0 -3
@@ -1,37 +1,46 @@
1
1
  var _a;
2
- import { parsePath } from "@plurnk/plurnk-grammar";
3
2
  import Owner from "./Owner.js";
4
- import { foldAuthorityIntoPath, schemeNameOf } from "./plurnk-uri.js";
3
+ import WorkerName, { WorkerNameError } from "./WorkerName.js";
4
+ import WorkerControlAddress from "./WorkerControlAddress.js";
5
+ import { entryPathnameOf, foldAuthorityIntoPath, renderAddress, renderTarget, schemeNameOf } from "./plurnk-uri.js";
5
6
  import Fork from "./fork.js";
6
7
  import WorkerCap from "./worker-cap.js";
7
- import { decodePathParens } from "./path-decode.js";
8
+ import { parsePath, PathSyntax } from "@plurnk/plurnk-contracts";
8
9
  import Namespace from "./namespace.js";
9
- import { DEFAULT_LOOP_FLAGS } from "./scheme-types.js";
10
+ import LoopFlagsReader from "./LoopFlagsReader.js";
10
11
  import ChannelWrite from "./ChannelWrite.js";
11
- import { LineMarkerOps, MimetypeBinary } from "../content/index.js";
12
+ import { assertEditBatchReceipt, assertEditReceipt, assertResourceEffects, editReceipt, LineMarkerOps, MimetypeBinary, PathMimetype, projectEditReceipt, } from "../content/index.js";
12
13
  import SchemeCtxImpl from "./caps/SchemeCtxImpl.js";
13
14
  import EntryCrud from "../schemes/_entry-crud.js";
14
- import EntryFind from "../schemes/_entry-find.js";
15
15
  import EntryOps from "../schemes/_entry-ops.js";
16
16
  import WorkspaceSettings from "./workspace-settings.js";
17
17
  import LoopLifecycle from "./LoopLifecycle.js";
18
- // SPEC §scheme-surface: writer must be in target scheme's manifest.writableBy.
18
+ import Results from "./results.js";
19
+ import { OperationFailureError } from "./results.js";
20
+ import EffectPolicy from "../schemes/EffectPolicy.js";
21
+ import { CoreSchemeAdapterBase } from "./CoreSchemeServices.js";
22
+ import { InvalidOperationResultError, NetworkAddress, } from "@plurnk/plurnk-schemes";
23
+ import DurableStatement from "./DurableStatement.js";
24
+ // SPEC {§scheme-surface}: writer must be in target scheme's manifest.writableBy.
19
25
  // OPEN/FOLD/READ/FIND are not gated — they curate the log or read, never mutating an entry.
20
26
  const MUTATING_OPS = new Set(["EDIT", "SEND", "COPY", "MOVE", "EXEC", "KILL", "FORK", "WORK"]);
21
- const pathnameFromPath = (path) => {
22
- if (path.kind === "regex")
23
- return path.raw; // regex source — parens are syntax, never encoded
24
- return decodePathParens(path.kind === "url" ? path.pathname : path.raw); // #239 item 4
25
- };
26
- // Op dispatch (§op-methods-op-dispatch): gates (writableBy, loop flags), the
27
+ // Op dispatch ({§op-methods-op-dispatch}): gates (writableBy, loop flags), the
27
28
  // engine-owned op orchestrations (COPY/MOVE/KILL/SEND/READ-fanout), scheme
28
29
  // routing, the durable log write, and the proposal pause.
29
30
  class Dispatcher {
31
+ static #failure(code, status, detail, fields = {}, extensions = {}) {
32
+ return Results.failure("engine:dispatcher", code, status, detail, fields, extensions);
33
+ }
34
+ static #statusResult(status, code, detail, fields = {}) {
35
+ return status >= 400
36
+ ? _a.#failure(code, status, detail, fields, { retryable: false })
37
+ : { ...fields, status };
38
+ }
30
39
  #db;
31
40
  #schemes;
32
41
  #mimetypes;
33
42
  #tokenize;
34
- #telemetry;
43
+ #notices;
35
44
  #proposals;
36
45
  // Boot-discovered runtime executors, late-injected on Engine — thunked.
37
46
  #executors;
@@ -40,30 +49,33 @@ class Dispatcher {
40
49
  #streamEventNotify;
41
50
  #wakeWorkerNotify;
42
51
  #injectWorker;
52
+ #branchWorker;
53
+ #branchCompletionGate;
43
54
  #cancelWorker;
44
55
  #cancelDescendants;
45
- // §send-premature-terminate/[102]<T> — the engine-owned park-deadline registry (loopId → seconds;
56
+ // {§send-premature-terminate}/SEND[202]<T> — the engine-owned park-deadline registry (loopId → seconds;
46
57
  // -1 = indefinite). The dispatcher WRITES at park; the daemon's drain park-exit consumes.
47
58
  #parkDeadlines;
48
59
  #searchGate;
49
- // §join-blocking-collect (#354) — loops with a READ(worker://running-child) armed this turn; a bare
50
- // SEND[102] parks on it (blocking join). Engine-owned, twin of #parkDeadlines.
60
+ // Per-turn running-worker READ obligations. {§join-blocking-collect}
51
61
  #joinTargets;
52
62
  #liveSubscriptions;
53
63
  #lifecycle;
54
64
  #preparedEdits = new WeakMap();
55
- constructor({ db, schemes, mimetypes, tokenize, telemetry, proposals, executors, loopSignal, streamEventNotify, wakeWorkerNotify, injectWorker, cancelWorker, cancelDescendants, searchGate, parkDeadlines, joinTargets, liveSubscriptions }) {
65
+ constructor({ db, schemes, mimetypes, tokenize, notices, proposals, executors, loopSignal, streamEventNotify, wakeWorkerNotify, injectWorker, branchWorker, branchCompletionGate, cancelWorker, cancelDescendants, searchGate, parkDeadlines, joinTargets, liveSubscriptions }) {
56
66
  this.#db = db;
57
67
  this.#schemes = schemes;
58
68
  this.#mimetypes = mimetypes;
59
69
  this.#tokenize = tokenize;
60
- this.#telemetry = telemetry;
70
+ this.#notices = notices;
61
71
  this.#proposals = proposals;
62
72
  this.#executors = executors;
63
73
  this.#loopSignal = loopSignal;
64
74
  this.#streamEventNotify = streamEventNotify;
65
75
  this.#wakeWorkerNotify = wakeWorkerNotify;
66
76
  this.#injectWorker = injectWorker;
77
+ this.#branchWorker = branchWorker;
78
+ this.#branchCompletionGate = branchCompletionGate;
67
79
  this.#cancelWorker = cancelWorker;
68
80
  this.#cancelDescendants = cancelDescendants;
69
81
  this.#parkDeadlines = parkDeadlines ?? new Map();
@@ -86,36 +98,78 @@ class Dispatcher {
86
98
  async #readEntry(scheme, pathname, ctx) {
87
99
  const handler = this.#schemes.get(scheme);
88
100
  const handlerCtx = this.#entryContext(scheme, ctx);
89
- if (typeof handler?.readEntry === "function" && handlerCtx !== null)
90
- return handler.readEntry(pathname, handlerCtx);
101
+ if (typeof handler?.readEntry === "function" && handlerCtx !== null) {
102
+ return Results.assert(await handler.readEntry(pathname, handlerCtx));
103
+ }
91
104
  const caps = handlerCtx?.entries;
92
- if (caps === undefined)
93
- return { status: 501, entry: null };
94
- const result = await caps.read(pathname, scheme === "prompt" ? "worker" : "commons");
95
- return {
105
+ if (caps === undefined) {
106
+ return _a.#failure("entry-read-not-implemented", 501, `The '${scheme}' scheme does not provide entry reads.`, { entry: null }, {
107
+ stage: "entry-read",
108
+ scheme,
109
+ target: renderAddress(scheme, pathname),
110
+ retryable: false,
111
+ });
112
+ }
113
+ const result = Results.assert(await caps.read(pathname, scheme === "prompt" ? "worker" : "commons"));
114
+ return Results.assert({
115
+ ...result,
96
116
  status: result.status,
97
117
  entry: result.entry === null
98
118
  ? null
99
119
  : { channels: { ...result.entry.channels }, tags: [...result.entry.tags] },
100
- };
120
+ });
101
121
  }
102
122
  async #writeEntry(scheme, pathname, entry, ctx) {
103
123
  const handler = this.#schemes.get(scheme);
104
124
  const handlerCtx = this.#entryContext(scheme, ctx);
105
- if (typeof handler?.writeEntry === "function" && handlerCtx !== null)
106
- return handler.writeEntry(pathname, entry, handlerCtx);
125
+ if (typeof handler?.writeEntry === "function" && handlerCtx !== null) {
126
+ return Results.assert(await handler.writeEntry(pathname, entry, handlerCtx));
127
+ }
107
128
  const caps = handlerCtx?.entries;
108
- if (caps === undefined)
109
- return { status: 501, created: false, entryId: null };
110
- return caps.write(pathname, entry);
129
+ if (caps === undefined) {
130
+ return _a.#failure("entry-write-not-implemented", 501, `The '${scheme}' scheme does not provide entry writes.`, { created: false, entryId: null }, {
131
+ stage: "entry-write",
132
+ scheme,
133
+ target: renderAddress(scheme, pathname),
134
+ retryable: false,
135
+ });
136
+ }
137
+ return Results.assert(await caps.write(pathname, entry, scheme === "prompt" ? "worker" : "commons"));
111
138
  }
112
139
  async #deleteEntry(scheme, pathname, ctx) {
113
140
  const handler = this.#schemes.get(scheme);
114
141
  const handlerCtx = this.#entryContext(scheme, ctx);
115
- if (typeof handler?.deleteEntry === "function" && handlerCtx !== null)
116
- return handler.deleteEntry(pathname, handlerCtx);
142
+ if (typeof handler?.deleteEntry === "function" && handlerCtx !== null) {
143
+ return Results.assert(await handler.deleteEntry(pathname, handlerCtx));
144
+ }
145
+ const caps = handlerCtx?.entries;
146
+ if (caps === undefined) {
147
+ return _a.#failure("entry-delete-not-implemented", 501, `The '${scheme}' scheme does not provide entry deletion.`, {}, {
148
+ stage: "entry-delete",
149
+ scheme,
150
+ target: renderAddress(scheme, pathname),
151
+ retryable: false,
152
+ });
153
+ }
154
+ return Results.assert(await caps.delete(pathname, scheme === "prompt" ? "worker" : "commons"));
155
+ }
156
+ async #deleteChannel(scheme, pathname, channel, ctx) {
157
+ const handler = this.#schemes.get(scheme);
158
+ const handlerCtx = this.#entryContext(scheme, ctx);
159
+ if (typeof handler?.deleteChannel === "function" && handlerCtx !== null) {
160
+ return Results.assert(await handler.deleteChannel(pathname, channel, handlerCtx));
161
+ }
117
162
  const caps = handlerCtx?.entries;
118
- return caps === undefined ? { status: 501 } : caps.delete(pathname);
163
+ if (caps === undefined) {
164
+ return _a.#failure("channel-delete-not-implemented", 501, `The '${scheme}' scheme does not provide channel deletion.`, {}, {
165
+ stage: "channel-delete",
166
+ scheme,
167
+ target: renderAddress(scheme, pathname),
168
+ channel,
169
+ retryable: false,
170
+ });
171
+ }
172
+ return Results.assert(await caps.delete(pathname, scheme === "prompt" ? "worker" : "commons", channel));
119
173
  }
120
174
  async #workspaceRoot(workspaceId) {
121
175
  if (this.#rootCache.has(workspaceId))
@@ -126,9 +180,8 @@ class Dispatcher {
126
180
  return root;
127
181
  }
128
182
  // {§fs-answer-in-canon} — a file-class target's engine-authored address COLUMNS carry
129
- // the canonical key; the tx JSON keeps the model's verbatim statement (history is never
130
- // rewritten — the one surviving spelling). An un-canonicalizable spelling keeps its raw
131
- // form: the row must still faithfully exist for the op that happened.
183
+ // the canonical key; tx keeps the operation spelling after the one durable request-evidence
184
+ // projection. An un-canonicalizable spelling keeps its raw form.
132
185
  async #canonColumns(target, workspaceId) {
133
186
  if (target.scheme !== null || target.pathname === null)
134
187
  return;
@@ -172,37 +225,61 @@ class Dispatcher {
172
225
  initial = denial;
173
226
  }
174
227
  else if (schemeName === null) {
175
- initial = { status: 400 };
228
+ initial = _a.#failure("target-required", 400, "EDIT requires a target scheme.", {}, { retryable: false });
176
229
  }
177
230
  else {
178
231
  const handler = this.#schemes.get(schemeName);
179
232
  const method = handler?.editBatch;
180
233
  const manifest = this.#schemes.manifestFor(schemeName);
181
234
  if (typeof method !== "function" || manifest === undefined) {
182
- initial = { status: 501 };
235
+ initial = _a.#failure("operation-not-implemented", 501, `Scheme '${schemeName}' does not implement EDIT batches.`, {}, {
236
+ scheme: schemeName,
237
+ operation: "EDIT",
238
+ retryable: false,
239
+ });
183
240
  }
184
241
  else {
185
242
  try {
186
243
  const addressedScheme = first.target?.kind === "url" ? first.target.scheme : schemeName;
187
- initial = await method.call(handler, group, new SchemeCtxImpl(schemeCtx, addressedScheme ?? schemeName, manifest, this.#liveSubscriptions));
244
+ initial = Results.assert(await method.call(handler, group, new SchemeCtxImpl(schemeCtx, addressedScheme ?? schemeName, manifest, this.#liveSubscriptions)));
188
245
  }
189
246
  catch (err) {
190
- initial = { status: 500, error: err instanceof Error ? err.message : String(err) };
247
+ if (err instanceof InvalidOperationResultError)
248
+ throw err;
249
+ console.error(`Scheme '${schemeName}' EDIT batch threw outside its operation result contract:`, err);
250
+ initial = _a.#failure("scheme-handler-threw", 500, `The '${schemeName}' scheme did not produce an EDIT result.`, {}, {
251
+ stage: "scheme-dispatch",
252
+ scheme: schemeName,
253
+ operation: "EDIT",
254
+ });
191
255
  }
192
256
  }
193
257
  }
194
258
  let resolveSettled;
195
259
  const settled = new Promise((resolve) => { resolveSettled = resolve; });
196
- const prepared = {
197
- first: true,
198
- index: 0,
260
+ const candidate = initial.editReceipt;
261
+ const expectedNormalizations = group.filter(({ lineMarker }) => lineMarker?.marks.length === 3).length;
262
+ if (initial.status < 400
263
+ && (initial.scopeNormalizations?.length ?? 0) !== expectedNormalizations) {
264
+ throw new InvalidOperationResultError(`EDIT batch normalized ${initial.scopeNormalizations?.length ?? 0} scope(s), expected ${expectedNormalizations}.`);
265
+ }
266
+ const batch = {
199
267
  initial,
200
268
  settled,
269
+ aggregate: candidate === undefined || candidate === null
270
+ ? undefined
271
+ : assertEditBatchReceipt(candidate),
201
272
  settle: resolveSettled,
202
273
  };
203
- this.#preparedEdits.set(first, prepared);
204
- for (const [index, statement] of group.slice(1).entries()) {
205
- this.#preparedEdits.set(statement, { ...prepared, first: false, index: index + 1 });
274
+ let normalizationIndex = 0;
275
+ for (const [index, statement] of group.entries()) {
276
+ const ownsNormalization = statement.lineMarker?.marks.length === 3 && initial.status < 400;
277
+ this.#preparedEdits.set(statement, {
278
+ first: index === 0,
279
+ index,
280
+ normalizationIndex: ownsNormalization ? normalizationIndex++ : null,
281
+ batch,
282
+ });
206
283
  }
207
284
  }
208
285
  }
@@ -210,29 +287,43 @@ class Dispatcher {
210
287
  const result = await this.#dispatchOne(context);
211
288
  if (context.statement.op === "EDIT") {
212
289
  const prepared = this.#preparedEdits.get(context.statement);
213
- if (prepared?.first === true)
214
- prepared.settle(result);
290
+ if (prepared?.first === true) {
291
+ const normalizations = prepared.batch.initial.scopeNormalizations;
292
+ prepared.batch.settle(normalizations === undefined
293
+ ? result
294
+ : Results.assert({ ...result, scopeNormalizations: normalizations }));
295
+ }
215
296
  }
216
297
  return result;
217
298
  }
218
299
  async #dispatchOne(context) {
219
- const { statement, workspaceId, workerId, loopId, turnId, sequence, origin, onDispatch, turnParseErrors } = context;
300
+ const { statement, workspaceId, workerId, loopId, turnId, sequence, origin, onDispatch, operationConstraint, } = context;
220
301
  const schemeCtx = this.#buildSchemeCtx({ workspaceId, workerId, loopId, turnId, origin });
221
302
  let result;
222
- let denial = this.#checkWritable(statement, origin);
303
+ let denial = operationConstraint !== undefined
304
+ && !operationConstraint.allowedOperations.includes(statement.op)
305
+ ? _a.#failure("operation-not-allowed", 409, `${statement.op} was not executed because ${operationConstraint.detail}.`, {}, {
306
+ constraint: operationConstraint.code,
307
+ allowedOperations: [...operationConstraint.allowedOperations],
308
+ retryable: false,
309
+ })
310
+ : null;
311
+ if (denial === null)
312
+ denial = this.#checkWritable(statement, origin);
223
313
  if (denial === null)
224
314
  denial = await this.#checkFlagsGate(statement, loopId);
225
315
  if (denial !== null) {
226
316
  result = denial;
227
317
  }
228
318
  else if (this.#readFansOut(statement)) {
229
- // READ honors FIND: a glob/folder scope or a matcher fans out to one log row per MATCH
230
- // (its own writeLogs), returning early. A READ never proposes, so it bypasses the
231
- // single-row path below. A bare entry, body-less, falls through to the direct read. #286
319
+ // Multi-resource scopes, relation matchers, and acquisition-scheme
320
+ // matchers use FIND for selection and write their own rows. An exact
321
+ // stored-resource content matcher remains a direct READ, preserving
322
+ // content-level outcomes such as 203.
232
323
  return await this.#handleReadFanout(statement, schemeCtx, { workerId, loopId, turnId, sequence, origin, onDispatch });
233
324
  }
234
325
  else {
235
- // SPEC §scheme-surface + plurnk-schemes#1: action-entry-as-outcome. Scheme-handler
326
+ // {§scheme-surface-exception-500} Scheme-handler
236
327
  // exceptions become the action-entry's outcome (status 500), not a
237
328
  // thrown bubble. The log_entry is the durable record; engine never
238
329
  // skips it. Logging failures (#writeLog throws) are NOT caught —
@@ -241,35 +332,38 @@ class Dispatcher {
241
332
  if (statement.op === "EDIT") {
242
333
  const prepared = this.#preparedEdits.get(statement);
243
334
  if (prepared === undefined) {
244
- result = { status: 500, error: "EDIT reached dispatch without a prepared resource batch" };
335
+ throw new InvalidOperationResultError("EDIT reached dispatch without a prepared resource batch.");
245
336
  }
246
337
  else {
247
- const settled = prepared.first ? prepared.initial : await prepared.settled;
248
- const aggregate = settled.editReceipt ?? prepared.initial.editReceipt;
249
- if (aggregate !== null && typeof aggregate === "object") {
250
- const receipt = aggregate;
251
- const effect = receipt.effects[prepared.index];
252
- if (effect === undefined)
253
- throw new Error(`EDIT receipt has no effect at index ${prepared.index}`);
254
- const { editReceipt: _editReceipt, ...withoutAggregate } = settled;
338
+ const settled = prepared.first
339
+ ? prepared.batch.initial
340
+ : await prepared.batch.settled;
341
+ const normalization = prepared.normalizationIndex === null
342
+ ? undefined
343
+ : settled.scopeNormalizations?.[prepared.normalizationIndex];
344
+ const { scopeNormalizations: _scopeNormalizations, ...projectedSettlement } = settled;
345
+ const statementResult = normalization === undefined
346
+ ? projectedSettlement
347
+ : { ...projectedSettlement, scopeNormalizations: [normalization] };
348
+ const aggregate = prepared.batch.aggregate;
349
+ if (aggregate !== undefined) {
350
+ const receipt = assertEditBatchReceipt(aggregate);
351
+ const { editReceipt: _editReceipt, ...withoutAggregate } = statementResult;
255
352
  result = {
256
353
  ...withoutAggregate,
257
- receipt: {
258
- revision: receipt.revision,
259
- unit: receipt.unit,
260
- before: receipt.before,
261
- after: receipt.after,
262
- effect,
263
- },
354
+ receipt: projectEditReceipt(receipt, prepared.index),
264
355
  };
265
356
  }
266
357
  else {
267
- result = settled;
358
+ result = statementResult;
268
359
  }
269
360
  }
270
361
  }
271
362
  else if (statement.op === "SEND" && statement.target === null) {
272
- result = await this.#handleSendBroadcast(statement, { workspaceId, workerId, loopId, turnId, turnParseErrors });
363
+ result = await this.#handleSendBroadcast(statement, { workspaceId, workerId, loopId, turnId, sequence });
364
+ }
365
+ else if (statement.op === "OPEN" || statement.op === "FOLD") {
366
+ result = await this.#runLogCuration(statement, schemeCtx);
273
367
  }
274
368
  else if (statement.op === "FORK" || statement.op === "WORK") {
275
369
  result = await this.#handleWorkerControl(statement, schemeCtx);
@@ -294,26 +388,31 @@ class Dispatcher {
294
388
  result = await this.#gatedExec(statement, schemeCtx, loopId, turnId);
295
389
  }
296
390
  else {
297
- result = await this.#run(schemeNameOf(statement.target), statement, schemeCtx); // §op-methods-op-dispatch
391
+ result = await this.#run(schemeNameOf(statement.target), statement, schemeCtx); // {§op-methods-op-dispatch}
298
392
  }
299
393
  }
300
- catch (err) { // a scheme exception becomes the op's 500 outcome — §scheme-surface-exception-500
301
- result = {
302
- status: 500,
303
- error: err instanceof Error ? err.message : String(err),
304
- };
394
+ catch (err) { // a scheme exception becomes the op's 500 outcome — {§scheme-surface-exception-500}
395
+ if (err instanceof InvalidOperationResultError)
396
+ throw err;
397
+ const scheme = schemeNameOf(statement.target);
398
+ console.error(`Scheme '${scheme ?? "unknown"}' ${statement.op} threw outside its operation result contract:`, err);
399
+ result = err instanceof OperationFailureError
400
+ ? err.result
401
+ : _a.#failure("scheme-handler-threw", 500, `The '${scheme ?? "unknown"}' scheme did not produce a ${statement.op} result.`, {}, {
402
+ stage: "scheme-dispatch",
403
+ scheme,
404
+ operation: statement.op,
405
+ });
305
406
  }
306
407
  }
307
- // §fold-open-meta-operations — OPEN/FOLD are render directives, not actions. They ARE
308
- // recorded (#382 a curation act with no forensic trace is how run43's task-frame fold
309
- // stayed invisible), but engine_render_log suppresses them from the packet, so the receipt
310
- // rents zero model-facing space — the original clutter concern, resolved by hide-not-drop.
311
- // §join-blocking-collect (#354) — Run.read on a still-running child returns an awaitWorker signal;
312
- // arm the join so THIS turn's bare SEND[102] parks (the blocking collect) instead of spinning.
408
+ // {§fold-open-meta-operations}persist OPEN/FOLD for forensics;
409
+ // packet rendering suppresses their successful receipts.
410
+ // A running-worker READ arms this turn's blocking collect.
411
+ // {§join-blocking-collect}
313
412
  if (typeof result.awaitWorker === "string")
314
413
  this.#joinTargets.add(loopId);
315
414
  const logEntryId = await this.#writeLog({ statement, result, workspaceId, workerId, loopId, turnId, sequence, origin });
316
- // §search-gate — register successful searches AFTER #writeLog stamps the runtime
415
+ // {§search-gate} — register successful searches AFTER #writeLog stamps the runtime
317
416
  // entry's coordinate onto result.attrs.pathname (the gate's dedup serves from it).
318
417
  if (statement.op === "EXEC" && result.status < 400) {
319
418
  const rt = ("signal" in statement && typeof statement.signal === "string" && statement.signal.length > 0) ? statement.signal : "sh";
@@ -323,19 +422,36 @@ class Dispatcher {
323
422
  this.#searchGate?.registerPending(loopId, turnId, rt, cmd, attrPath);
324
423
  }
325
424
  onDispatch?.(logEntryId);
326
- // Proposal lifecycle (SPEC.md §engine-rails + §methods loop.resolve; §proposal-202-pauses). When a
425
+ // Proposal lifecycle (SPEC.md {§engine-rails} + {§methods-proposal-resolve}; {§proposal-202-pauses}). When a
327
426
  // side-effecting op returns status 202 (a broadcast SEND[202] park is model
328
- // speech, not a proposal — #isProposal, #255), the entry is written
427
+ // speech, not a proposal — #isProposal), the entry is written
329
428
  // state='proposed'; dispatch then PAUSES on a per-entry waiter until
330
- // resolution arrives via Engine.resolveProposal (from the loop/resolve RPC,
331
- // auto listener, or timeout). The post-resolution status replaces 202 in the
429
+ // resolution arrives via Engine.resolveProposal (from a client-interface resume,
430
+ // core-owned disposition, or timeout). The post-resolution status replaces 202 in the
332
431
  // result the caller sees, so runTurn never branches on a pending state.
333
432
  if (_a.#isProposal(statement, result)) {
334
- // Effect-gated auto-run (read/pure runtimes, plurnk-service#182):
335
- // no human gate, no loop/proposal notification. Accept + apply
336
- // in-process; the model sees the outcome directly, never a review.
337
- if (result.attrs?.inline === true) {
338
- const effective = await this.#proposals.workerApply(statement, result, { decision: "accept" }, { workspaceId, workerId, loopId, turnId });
433
+ // Effect-gated auto-run (read/pure runtimes, {§exec-readpure-ungated}):
434
+ // EXEC stores its one canonical effect fact before admission. Reuse
435
+ // that exact fact here; no human gate or loop/proposal notification.
436
+ const effect = result.attrs?.effect;
437
+ let autoAccept = false;
438
+ if (statement.op === "EXEC") {
439
+ if (!EffectPolicy.isEffect(effect)) {
440
+ throw new InvalidOperationResultError("EXEC proposal omitted its canonical effect fact.");
441
+ }
442
+ autoAccept = EffectPolicy.decide(effect) === "auto";
443
+ }
444
+ if (autoAccept) {
445
+ const initialSettlement = await this.#proposals.workerApply(statement, result, { decision: "accept" }, { workspaceId, workerId, loopId, turnId });
446
+ const settlement = _a.#settleProposalEffects(result, initialSettlement);
447
+ const effective = await this.#settleMoveProposal({
448
+ statement,
449
+ result,
450
+ settlement,
451
+ ctx: schemeCtx,
452
+ ids: { workspaceId, workerId, loopId, turnId },
453
+ });
454
+ this.#recordEditSettlement(statement, effective);
339
455
  return this.#proposals.applyResolution(logEntryId, effective);
340
456
  }
341
457
  // Register the resolution waiter SYNCHRONOUSLY before any await
@@ -343,56 +459,43 @@ class Dispatcher {
343
459
  // awaits the onDispatch callback and immediately resolves) must
344
460
  // find the waiter registered — adding an await between insert
345
461
  // and waiter-registration would open a race window.
346
- const resolutionPromise = this.#proposals.awaitResolution(logEntryId);
347
- // Notify external listeners (Daemon broadcasts loop/proposal;
348
- // auto listener auto-resolves) BEFORE awaiting they may
349
- // resolve synchronously inside their handlers.
350
- const target = this.#extractTarget(statement.target);
351
- await this.#canonColumns(target, workspaceId); // {§fs-answer-in-canon} — compare in the same canon the rows store
352
- const flags = await this.#loadLoopFlags(loopId); // the proposal carries loop authority — §proposal-ownership-notification
353
- // #note10 if the target diverged on disk this turn, the model's EDIT is based
354
- // on a stale read; flag it so a auto resolution rejects instead of clobbering.
355
- const diverged = await this.#db.engine_target_diverged_this_turn.get({ worker_id: workerId, turn_id: turnId, scheme: target.scheme, pathname: target.pathname });
356
- const event = {
357
- logEntryId, workspaceId, workerId, loopId, turnId,
358
- op: statement.op,
359
- target: { scheme: target.scheme, pathname: target.pathname },
360
- body: typeof result.body === "string" ? result.body : "",
361
- attrs: (result.attrs ?? {}),
362
- flags,
363
- staleClobberRisk: diverged !== undefined,
364
- };
365
- this.#proposals.notifyPending(event);
462
+ // Core derives one validated projection from the durable row for both
463
+ // this live event and reconnect discovery ({§proposal-projection}). Its
464
+ // disposition is also the one automatic settlement decision: policy is
465
+ // not an observer and cannot silently degrade into client ownership.
466
+ let resolutionPromise;
467
+ try {
468
+ resolutionPromise = this.#proposals.awaitResolution(logEntryId);
469
+ const event = await this.#proposals.pending(logEntryId);
470
+ this.#proposals.settleOwned(event);
471
+ this.#proposals.notifyPending(event);
472
+ }
473
+ catch (cause) {
474
+ await this.#proposals.failPreparation(logEntryId, cause);
475
+ throw cause;
476
+ }
366
477
  const resolution = await resolutionPromise;
367
478
  // Run the scheme's applyResolution hook on accept (writes the
368
- // file, spawns the process, etc.). If applyResolution returns a
369
- // 4xx/5xx or throws, the resolution is downgraded to a reject
370
- // with the failure outcome engine treats it like a client
371
- // rejection.
372
- const effective = await this.#proposals.workerApply(statement, result, resolution, { workspaceId, workerId, loopId, turnId });
373
- // MOVE into a proposed dest: the deferred source-delete fires ONLY now,
374
- // after the dest write landed (accept). On reject the source survives.
375
- if (effective.decision === "accept") {
376
- const moveSource = result.attrs?.moveSource;
377
- if (moveSource !== undefined) {
378
- const srcHandler = this.#schemes.get(moveSource.scheme);
379
- if (srcHandler !== undefined) {
380
- const del = await this.#deleteEntry(moveSource.scheme, moveSource.pathname, schemeCtx);
381
- // A host-effecting source-delete PROPOSES (202); the MOVE proposal already gated the whole
382
- // create+kill, so apply the source-delete now — never raise a second review for one MOVE.
383
- const sourceCtx = this.#handlerContext(moveSource.scheme, schemeCtx);
384
- if (del.status === 202 && del.attrs !== undefined && typeof srcHandler.applyResolution === "function" && sourceCtx !== null)
385
- await srcHandler.applyResolution({ attrs: del.attrs }, sourceCtx);
386
- }
387
- }
388
- }
479
+ // file, spawns the process, etc.). Its operation result is
480
+ // preserved: an apply failure keeps its original status and
481
+ // Problem Details instead of masquerading as a client rejection.
482
+ const initialSettlement = await this.#proposals.workerApply(statement, result, resolution, { workspaceId, workerId, loopId, turnId });
483
+ const settlement = _a.#settleProposalEffects(result, initialSettlement);
484
+ const effective = await this.#settleMoveProposal({
485
+ statement,
486
+ result,
487
+ settlement,
488
+ ctx: schemeCtx,
489
+ ids: { workspaceId, workerId, loopId, turnId },
490
+ });
491
+ this.#recordEditSettlement(statement, effective);
389
492
  const post = await this.#proposals.applyResolution(logEntryId, effective);
390
493
  return post;
391
494
  }
392
495
  return result;
393
496
  }
394
- // op.look (#283) — resolve a READ and return its content WITHOUT writing a
395
- // log_entries row: the client's off-run inspection primitive (LOOK → READ,
497
+ // op-look}: resolve a READ and return its content without writing a
498
+ // log_entries row: the client's out-of-band inspection primitive (LOOK → READ,
396
499
  // invisible to the model). READ never mutates and never proposes, so this is
397
500
  // dispatch's resolve path minus #writeLog. Runs on the client loop, so the
398
501
  // human's inspection is never constrained by a model loop's flags. {§op-look}
@@ -407,6 +510,80 @@ class Dispatcher {
407
510
  return denial;
408
511
  return this.#run(schemeNameOf(statement.target), statement, schemeCtx);
409
512
  }
513
+ // Resolve the client selector through the owning scheme before persistence
514
+ // is consulted. Public schemes choose a semantic owner; core-owned authority
515
+ // schemes may return the already-authorized principal key.
516
+ async resolveEntryAddress(context) {
517
+ const { target, workspaceId, workerId } = context;
518
+ const routedScheme = schemeNameOf(target);
519
+ if (routedScheme === null)
520
+ return null;
521
+ const handler = this.#schemes.get(routedScheme);
522
+ const manifest = this.#schemes.manifestFor(routedScheme);
523
+ if (handler === undefined || manifest?.category !== "data")
524
+ return null;
525
+ const addressedScheme = target.kind === "url" ? target.scheme : routedScheme;
526
+ const coreCtx = this.#buildSchemeCtx({
527
+ workspaceId,
528
+ workerId,
529
+ loopId: 0,
530
+ turnId: 0,
531
+ origin: "client",
532
+ });
533
+ const schemeCtx = new SchemeCtxImpl(coreCtx, addressedScheme, manifest, this.#liveSubscriptions);
534
+ const resolved = handler.resolveEntryAddress === undefined
535
+ ? { pathname: entryPathnameOf(target), owner: "commons" }
536
+ : await handler.resolveEntryAddress(target, schemeCtx);
537
+ if (resolved === null)
538
+ return null;
539
+ if (typeof resolved.pathname !== "string") {
540
+ throw new TypeError(`Scheme '${routedScheme}' returned an invalid entry pathname.`);
541
+ }
542
+ let ownerId;
543
+ if ("ownerId" in resolved) {
544
+ if (!(handler instanceof CoreSchemeAdapterBase)) {
545
+ throw new TypeError(`Scheme '${routedScheme}' returned a core-only entry owner id.`);
546
+ }
547
+ ownerId = resolved.ownerId;
548
+ }
549
+ else if (resolved.owner === "worker") {
550
+ ownerId = workerId;
551
+ }
552
+ else if (resolved.owner === "commons") {
553
+ ownerId = await Owner.commonsId(this.#db, workspaceId);
554
+ }
555
+ else {
556
+ throw new TypeError(`Scheme '${routedScheme}' returned an invalid entry owner.`);
557
+ }
558
+ if (!Number.isSafeInteger(ownerId) || ownerId < 1) {
559
+ throw new TypeError(`Scheme '${routedScheme}' returned an invalid entry owner id.`);
560
+ }
561
+ const rendered = target.kind === "url"
562
+ ? renderTarget({ ...target, fragment: null })
563
+ : renderTarget({ scheme: null, pathname: target.raw, fragment: null });
564
+ if (rendered === null)
565
+ throw new TypeError("Resolved entry target did not render.");
566
+ return {
567
+ ownerId,
568
+ scheme: manifest.storedScheme ?? addressedScheme,
569
+ pathname: resolved.pathname,
570
+ target: rendered,
571
+ };
572
+ }
573
+ // An accepted EXEC reads a non-file source through the same registered
574
+ // handler and addressed context as an authored READ. {§exec-target-routing}
575
+ async readExecSource(statement, ctx) {
576
+ const schemeName = schemeNameOf(statement.target);
577
+ const manifest = schemeName === null ? undefined : this.#schemes.manifestFor(schemeName);
578
+ if (manifest !== undefined && manifest.category !== "data") {
579
+ return _a.#failure("exec-source-not-data", 501, `Scheme '${schemeName}' is not a data source for EXEC.`, {}, {
580
+ scheme: schemeName,
581
+ category: manifest.category,
582
+ retryable: false,
583
+ });
584
+ }
585
+ return Results.assertReadResult(await this.#run(schemeName, statement, ctx));
586
+ }
410
587
  #buildSchemeCtx(ids) {
411
588
  const { workspaceId, workerId, loopId, turnId, origin } = ids;
412
589
  return {
@@ -419,33 +596,16 @@ class Dispatcher {
419
596
  injectWorker: this.#injectWorker,
420
597
  mimetypes: this.#mimetypes,
421
598
  tokenize: this.#tokenize,
422
- pushTelemetry: (event) => this.#telemetry.push(workspaceId, loopId, event),
599
+ pushNotice: (notice) => this.#notices.push(workspaceId, loopId, notice),
423
600
  executors: this.#executors(),
424
601
  };
425
602
  }
426
- // Loads loops.flags (json column) and merges over DEFAULT_LOOP_FLAGS so
427
- // missing keys read as their documented defaults. Single read site —
428
- // ProposalPendingEvent.flags is constructed from this, and listeners
429
- // (Daemon broadcast, loop auto) share the result.
430
- async #loadLoopFlags(loopId) {
431
- const row = await this.#db.engine_get_loop_flags.get({ loop_id: loopId });
432
- if (row === undefined)
433
- return DEFAULT_LOOP_FLAGS;
434
- try {
435
- const parsed = JSON.parse(row.flags);
436
- return { ...DEFAULT_LOOP_FLAGS, ...parsed };
437
- }
438
- catch {
439
- return DEFAULT_LOOP_FLAGS;
440
- }
441
- }
442
- // SPEC §scheme-surface: engine rejects writes whose origin is outside the target
603
+ // SPEC {§scheme-surface}: engine rejects writes whose origin is outside the target
443
604
  // scheme's manifest.writableBy.
444
605
  // - Read-side ops (READ, FIND, OPEN, FOLD) are not gated.
445
606
  // - SEND broadcast (path=null) has no target scheme; not gated.
446
607
  // - COPY: dst scheme writableBy applies.
447
608
  // - MOVE: both src (delete) and dst (write) schemes' writableBy apply.
448
- // - Schemes without a manifest are not gated (legacy / future allowance).
449
609
  #checkWritable(statement, origin) {
450
610
  if (!MUTATING_OPS.has(statement.op))
451
611
  return null;
@@ -456,13 +616,13 @@ class Dispatcher {
456
616
  if (statement.op === "EXEC") {
457
617
  return this.#denyIfDisallowed("exec", origin);
458
618
  }
459
- // Run control (FORK/WORK → worker://<name>, spawn or fork) is gated by worker://'s writableBy — its
619
+ // Worker control (FORK/WORK → worker://<name>, spawn or fork) is gated by worker://'s writableBy — its
460
620
  // body is a seed prompt, not a dst path, so the entry-COPY dst-parse below doesn't apply.
461
- // §machine-processes
621
+ // {§machine-processes}
462
622
  if (this.#isWorkerControl(statement))
463
623
  return this.#denyIfDisallowed("worker", origin);
464
624
  if (statement.op === "COPY" || statement.op === "MOVE") {
465
- const dst = statement.op === "COPY" ? (statement.body === null ? null : parsePath(statement.body)) : statement.body;
625
+ const dst = statement.body?.target ?? null;
466
626
  const dstScheme = schemeNameOf(dst);
467
627
  const dstDenial = this.#denyIfDisallowed(dstScheme, origin);
468
628
  if (dstDenial !== null)
@@ -480,17 +640,19 @@ class Dispatcher {
480
640
  const target = schemeNameOf(statement.target);
481
641
  return this.#denyIfDisallowed(target, origin);
482
642
  }
483
- // §search-gate (#406, owner ruling) search runtimes only; everything else dispatches
484
- // untouched. A DUPLICATE (same runtime+command this loop) STRIKES AND SERVES: 409 (the
485
- // strike rail counts the turn failure; the failed-ops gate holds the terminal a turn)
486
- // carrying the prior ranked digest re-read live — the model sees its results again, the
487
- // engine never re-fetches, no provenance prose. The per-turn CAP is flood control: 429.
643
+ // {§search-gate} gate only configured search runtimes; duplicates serve
644
+ // the prior durable digest and the per-turn cap refuses without execution.
488
645
  async #gatedExec(statement, ctx, loopId, turnId) {
489
646
  const runtime = ("signal" in statement && typeof statement.signal === "string" && statement.signal.length > 0) ? statement.signal : "sh";
490
647
  const command = ("body" in statement && typeof statement.body === "string") ? statement.body : "";
491
648
  const verdict = this.#searchGate?.check(loopId, turnId, runtime, command) ?? { verdict: "pass" };
492
649
  if (verdict.verdict === "capped") {
493
- return { status: 429, error: `Per-turn search limit reached (${verdict.cap}).` };
650
+ return _a.#failure("search-limit-reached", 429, `This turn already used its ${verdict.cap} permitted searches.`, {}, {
651
+ searchLimit: verdict.cap,
652
+ stage: "search-admission",
653
+ recovery: "Continue without another search in this turn.",
654
+ retryable: false,
655
+ });
494
656
  }
495
657
  if (verdict.verdict === "duplicate") {
496
658
  // {§stream-owner-scoped} — the prior ranked digest is the CALLER's own stream entry.
@@ -501,7 +663,10 @@ class Dispatcher {
501
663
  results = JSON.parse(raw);
502
664
  }
503
665
  catch { /* non-JSON results serve verbatim */ }
504
- return { status: 409, results };
666
+ return _a.#failure("duplicate-search", 409, "This search already ran in the current loop; the prior results are attached.", { results }, {
667
+ recovery: "Use the attached prior results.",
668
+ retryable: false,
669
+ });
505
670
  }
506
671
  return this.#run("exec", statement, ctx);
507
672
  }
@@ -511,115 +676,156 @@ class Dispatcher {
511
676
  const handler = this.#schemes.get(schemeName);
512
677
  if (handler === undefined)
513
678
  return null;
514
- const manifest = handler.constructor.manifest;
679
+ const manifest = this.#schemes.manifestFor(schemeName);
515
680
  if (manifest === undefined)
516
- return null;
681
+ throw new Error(`registered scheme '${schemeName}' has no manifest`);
517
682
  if (manifest.writableBy.includes(origin))
518
683
  return null;
519
- return { status: 403, error: `writer '${origin}' is not in writableBy for scheme '${schemeName}'` }; // §scheme-surface-writableby-403
684
+ return _a.#failure("writer-forbidden", 403, `Writer '${origin}' cannot modify scheme '${schemeName}'.`, {}, {
685
+ writer: origin,
686
+ scheme: schemeName,
687
+ allowedWriters: [...manifest.writableBy],
688
+ retryable: false,
689
+ }); // {§scheme-surface-writableby-403}
520
690
  }
521
691
  // Per-loop flag gating. Schemes self-declare their flag affinity in
522
692
  // their manifest (excludedInAsk / requiresWeb /
523
693
  // requiresInteraction); SchemeRegistry.resolveForLoop returns the
524
- // active set under the loop's persisted flags. Anything outside the
525
- // set returns 403 action-entry-as-outcome carries the rejection.
694
+ // active set under the loop's persisted flags. A registered scheme outside
695
+ // that set returns 403; unknown names continue to their operation owner for
696
+ // the ordinary registration failure. Action-entry-as-outcome carries either.
526
697
  async #checkFlagsGate(statement, loopId) {
527
698
  // Broadcast SEND has no scheme to gate.
528
699
  if (statement.op === "SEND" && statement.target === null)
529
700
  return null;
530
- const flags = await this.#loadLoopFlags(loopId);
701
+ const flags = await LoopFlagsReader.read(this.#db, loopId);
531
702
  // Fast path: default flags gate nothing. (auto never gates.)
532
703
  if (!flags.noWeb && !flags.noInteraction && flags.mode === "act")
533
704
  return null;
534
- // §mode-ask-read-only — the ancient contract: an ask-mode loop NEVER changes the world. The
705
+ // {§mode-ask-read-only} — the ancient contract: an ask-mode loop NEVER changes the world. The
535
706
  // filesystem writes (EDIT/COPY-dest/MOVE/KILL touching the `file` scheme — each proposes disk
536
- // egress, §membership) are refused HERE, regardless of the scheme's read-activity, because
707
+ // egress, {§membership}) are refused HERE, regardless of the scheme's read-activity, because
537
708
  // `file` stays active for READs. The EXEC host runtime is refused by its excludedInAsk scheme
538
709
  // below. This lived only in SPEC (line 65) with no anchor → no guard → it silently regressed.
539
710
  if (flags.mode === "ask") {
540
711
  const isFile = (t) => schemeNameOf(t) === "file";
541
712
  // Each branch narrows statement.op so statement.body is correctly typed (COPY dest is a
542
- // string to parse; MOVE dest is already a path). EDIT/KILL write the target; COPY writes
543
- // the dest; MOVE deletes the source AND writes the dest any `file` touch is a write.
713
+ // resource selection). EDIT/KILL write the target; COPY writes the dest;
714
+ // MOVE deletes the source AND writes the dest - any `file` touch is a write.
544
715
  let writesFilesystem = false;
545
716
  if (statement.op === "EDIT" || statement.op === "KILL")
546
717
  writesFilesystem = isFile(statement.target);
547
718
  else if (statement.op === "COPY")
548
- writesFilesystem = isFile(statement.body === null ? null : parsePath(statement.body));
719
+ writesFilesystem = isFile(statement.body?.target ?? null);
549
720
  else if (statement.op === "MOVE")
550
- writesFilesystem = isFile(statement.target) || isFile(statement.body);
721
+ writesFilesystem = isFile(statement.target) || isFile(statement.body?.target ?? null);
551
722
  if (writesFilesystem) {
552
- return { status: 403, error: `'${statement.op}' cannot change the filesystem in an ask-mode loop ask is read-only (no side-effecting ops). Answer or advise the user directly; an act-mode loop is required to edit files.` };
723
+ return _a.#failure("ask-mode-read-only", 403, `${statement.op} cannot change the filesystem in an ask-mode loop.`, {}, {
724
+ mode: flags.mode,
725
+ operation: statement.op,
726
+ recovery: "Answer or advise the user without changing the filesystem.",
727
+ retryable: false,
728
+ });
553
729
  }
554
730
  }
555
731
  const active = this.#schemes.resolveForLoop(flags);
556
- // #367 the steer NAMES the restriction and says DO NOT RETRY: the old vague "inactive under
557
- // current loop flags" invited an identical re-emit each turn → the StrikeRail's 508. An
558
- // unavailable op is not a failing one; the model must change course, not repeat.
732
+ // {§tools-loop-affinity}: name the non-retryable restriction so the model changes course.
559
733
  const restriction = flags.mode === "ask"
560
734
  ? "this is an ask-mode (read-only) loop — you cannot run commands or take host actions here"
561
735
  : flags.noWeb && flags.noInteraction ? "web and interaction are disabled for this loop"
562
736
  : flags.noWeb ? "web access is disabled for this loop"
563
737
  : "interaction is disabled for this loop";
564
- const check = (target) => {
565
- const scheme = schemeNameOf(target);
566
- if (scheme === null)
738
+ const checkScheme = (scheme) => {
739
+ if (scheme === null || !this.#schemes.has(scheme))
567
740
  return null;
568
741
  if (active.has(scheme))
569
742
  return null;
570
- return { status: 403, error: `'${scheme}' is unavailable: ${restriction}. Do NOT retry it — it is unavailable, not failing; answer or advise the user directly (an act-mode loop is required to use it).` };
743
+ return _a.#failure("scheme-unavailable", 403, `Scheme '${scheme}' is unavailable because ${restriction}.`, {}, {
744
+ scheme,
745
+ recovery: "Answer or advise the user without using the unavailable scheme.",
746
+ retryable: false,
747
+ });
571
748
  };
749
+ const check = (target) => checkScheme(schemeNameOf(target));
572
750
  if (this.#isWorkerControl(statement))
573
751
  return check(statement.target); // body is a spawn/fork task, not a dst path
574
752
  if (statement.op === "COPY" || statement.op === "MOVE") {
575
- return check(statement.target) ?? check(statement.op === "COPY" ? (statement.body === null ? null : parsePath(statement.body)) : statement.body);
753
+ return check(statement.target) ?? check(statement.body?.target ?? null);
754
+ }
755
+ // {§exec-target-routing} — the operation owner and a non-file source
756
+ // are independent authorities; local/file targets stay executor-local.
757
+ if (statement.op === "EXEC") {
758
+ const operationDenial = checkScheme("exec");
759
+ if (operationDenial !== null)
760
+ return operationDenial;
761
+ const sourceScheme = schemeNameOf(statement.target);
762
+ return sourceScheme === null || sourceScheme === "file" ? null : checkScheme(sourceScheme);
576
763
  }
577
764
  return check(statement.target);
578
765
  }
579
- // Run control is FORK/WORK (grammar 0.74.55), not COPY — its body
766
+ // Worker control is FORK/WORK (grammar 0.74.55), not COPY — its body
580
767
  // is the new worker's seed prompt, not a destination path. The COPY gates and #handleCopy
581
768
  // branch on this so they never parse the prompt as a dst path.
582
769
  #isWorkerControl(statement) {
583
- return statement.op === "FORK" || statement.op === "WORK"; // run control targets worker://<name> (grammar 0.74.55)
584
- }
585
- // FORK/WORK(worker://<name>):task run control (grammar 0.74.55):
586
- // worker://self → FORK: deep-copy the current worker's log into a new sister (Fork), then
587
- // continue it with the prompt (§machine-processes-fork-copies-the-log).
588
- // • worker://<name> → SPAWN: a fresh sister (empty log) named <name>, started on the prompt.
589
- // A LIVE sister already holding <name> is a 409 conflict; a free or terminated name is
590
- // reclaimed (§worker-scheme-spawn). The self form is fork; only a name spawns.
591
- // Both ride the daemon inject and obey the active-runs cap (508, §worker-scheme-cap).
592
- // FORK/WORK — run control (grammar 0.74.55). Both name a NEW run in the target authority
593
- // (worker://<name>) and carry its seed task in the body. WORK spawns a fresh worker; FORK branches
594
- // the current worker's log into a named sister. Replaces the COPY(worker://) overload — one verb, one
595
- // intent, so the model never conflates the target slot with the body (grammar#52).
770
+ return statement.op === "FORK" || statement.op === "WORK"; // worker control targets worker://<name> (grammar 0.74.55)
771
+ }
772
+ // WORK and FORK name the new worker in the target authority and carry its seed task in the body.
773
+ // Their distinct fresh/branched histories are specified by {§worker-scheme-spawn} and {§worker-scheme-fork}.
596
774
  async #handleWorkerControl(statement, ctx) {
597
- const target = statement.target;
598
- if (target === null)
599
- return { status: 400, error: `${statement.op} requires a worker target (${statement.op}(worker://<name>))` };
600
- const name = target.kind === "url" ? (target.hostname ?? "") : ""; // §worker-scheme — run is the AUTHORITY (worker://<name>), not the path
601
- if (name === "")
602
- return { status: 400, error: `${statement.op} requires a worker name (worker://<name>)` };
603
- if (name === "self")
604
- return { status: 400, error: `'self' is the current run — ${statement.op} names a NEW run (worker://<name>)` };
605
- // {§entry-owner} 'commons'/'plurnk' are the reserved owner rows, '~' the self-sigil; no spawn takes them.
606
- if (Owner.RESERVED.has(name))
607
- return { status: 400, error: `'${name}' is a reserved worker name` };
775
+ const address = WorkerControlAddress.resolve(statement.target, statement.op);
776
+ if (!address.ok)
777
+ return address.result;
778
+ const name = address.authority;
779
+ try {
780
+ WorkerName.assert(name); // {§worker-name-minting}
781
+ }
782
+ catch (error) {
783
+ if (!(error instanceof WorkerNameError))
784
+ throw error;
785
+ return _a.#failure(`worker-${error.code}`, 400, error.message, {}, {
786
+ operation: statement.op,
787
+ worker: error.workerName,
788
+ recovery: error.recovery,
789
+ retryable: false,
790
+ });
791
+ }
608
792
  if (ctx.injectWorker === undefined)
609
- throw new Error("run control: injectWorker capability absent");
793
+ throw new Error("worker control: injectWorker capability absent");
610
794
  const denied = await WorkerCap.deny(this.#db, ctx.workspaceId);
611
795
  if (denied !== null)
612
796
  return denied;
613
- const prompt = typeof statement.body === "string" ? statement.body : "";
614
- // §worker-delegation-inherits-flags — authority flows down the delegation edge: the child's live
797
+ const prompt = statement.body;
798
+ // {§worker-delegation-inherits-flags} — authority flows down the delegation edge: the child's live
615
799
  // loop runs with ITS DELEGATOR'S flags. A flagless (non-auto) child's every side-effecting op
616
800
  // proposes into a resolver-less void — 300s auto-cancel per attempt was the fan-out wedge.
617
- const flags = await this.#loadLoopFlags(ctx.loopId);
618
- // A name is frozen per worker but reclaimable across time (§machine-processes-worker-origin): a LIVE
801
+ const flags = await LoopFlagsReader.read(this.#db, ctx.loopId);
802
+ // A name is frozen per worker but reclaimable across time ({§machine-processes-worker-origin}): a LIVE
619
803
  // sister holding it is a 409 (legible, never a raw UNIQUE 500); a free/terminated name reclaims.
620
804
  const live = await this.#db.worker_live_by_name.get({ workspace_id: ctx.workspaceId, name });
621
- if (live !== undefined)
622
- return { status: 409, error: `worker '${name}' is already running` };
805
+ if (live !== undefined) {
806
+ return _a.#failure("worker-already-running", 409, `Worker '${name}' is already running.`, {}, { worker: name, retryable: false });
807
+ }
808
+ if (typeof statement.signal === "string") {
809
+ if (this.#branchWorker === undefined)
810
+ throw new Error("branch worker control: branchWorker capability absent");
811
+ const child = await this.#branchWorker({
812
+ workspaceId: ctx.workspaceId,
813
+ parentWorkerId: ctx.workerId,
814
+ parentLoopId: ctx.loopId,
815
+ parentTurnId: ctx.turnId,
816
+ op: statement.op,
817
+ name,
818
+ branch: statement.signal,
819
+ prompt,
820
+ flags,
821
+ origin: ctx.writer,
822
+ });
823
+ return {
824
+ status: 200,
825
+ body: name,
826
+ attrs: { branch: statement.signal, workerId: child.workerId, loopId: child.loopId },
827
+ };
828
+ }
623
829
  if (statement.op === "FORK") {
624
830
  // Branch the current worker's log into a named sister.
625
831
  const branchWorkerId = await Fork.fork(this.#db, ctx.workerId, name);
@@ -631,154 +837,164 @@ class Dispatcher {
631
837
  workspace_id: ctx.workspaceId, name, parent_worker_id: ctx.workerId, origin: ctx.writer,
632
838
  });
633
839
  if (row === undefined)
634
- throw new Error("run spawn: run insert returned no row");
840
+ throw new Error("worker spawn: worker insert returned no row");
635
841
  await ctx.injectWorker({ workspaceId: ctx.workspaceId, workerId: row.id, prompt, flags });
636
842
  return { status: 200, body: name };
637
843
  }
638
844
  async #handleCopy(statement, ctx) {
639
- if (statement.op !== "COPY")
640
- throw new Error("unreachable");
641
- const srcPath = statement.target;
642
- // COPY is entry-copy only — run control (spawn/fork) moved to the FORK/WORK verbs
643
- // (grammar 0.74.55). COPY's body is a dest path (grammar §COPY); an unparseable dest → 400.
644
- const dstPath = statement.body === null ? null : parsePath(statement.body);
645
- if (srcPath === null)
646
- return { status: 400, error: "COPY requires source path" };
647
- if (dstPath === null)
648
- return { status: 400, error: "COPY destination must be a parseable path in the body slot" };
649
- return await this.#copyOrchestration({ statement, srcPath, dstPath, ctx });
845
+ if (statement.target === null) {
846
+ return _a.#failure("copy-source-required", 400, "COPY requires a source path.", {}, { retryable: false });
847
+ }
848
+ if (statement.body === null) {
849
+ return _a.#failure("copy-destination-required", 400, "COPY requires a destination.", {}, { retryable: false });
850
+ }
851
+ return this.#copyOrchestration({
852
+ statement,
853
+ source: {
854
+ target: statement.target,
855
+ lineMarker: statement.lineMarker,
856
+ },
857
+ destination: statement.body,
858
+ ctx,
859
+ });
650
860
  }
651
861
  async #handleMove(statement, ctx) {
652
- if (statement.op !== "MOVE")
653
- throw new Error("unreachable");
654
- const srcPath = statement.target;
655
- const dstPath = statement.body;
656
- if (srcPath === null)
657
- return { status: 400, error: "MOVE requires source path" };
658
- // MOVE is relocation only deletion is KILL's job (§move, §move-dev-null-not-special). The /dev/null
659
- // and null-body delete-by-MOVE back-compat is retired: no silent debt.
660
- if (dstPath === null)
661
- return { status: 400, error: "MOVE requires a destination; use KILL to delete" }; // §move-null-body-400
662
- const srcSchemeName = schemeNameOf(srcPath);
663
- if (srcSchemeName === null)
664
- return { status: 400, error: "MOVE source must be a URL path with a scheme" };
665
- if (!this.#schemes.has(srcSchemeName))
666
- return { status: 501 };
667
- // Relocation: COPY then DELETE source (§move-relocation-deletes-source).
668
- const copyResult = await this.#copyOrchestration({ statement, srcPath, dstPath, ctx });
669
- if (copyResult.status >= 400)
670
- return copyResult;
671
- const srcPathname = pathnameFromPath(srcPath);
672
- // If the dest write is a pending proposal (file dest → §membership review), the
673
- // source-delete MUST wait until the dest actually lands — a rejected
674
- // proposal would otherwise lose the source. Thread it into the resolution:
675
- // dispatch deletes the source AFTER the dest applies on accept.
676
- if (copyResult.status === 202) {
677
- return { ...copyResult, attrs: { ...copyResult.attrs, moveSource: { scheme: srcSchemeName, pathname: srcPathname } } };
678
- }
679
- const delResult = await this.#deleteEntry(srcSchemeName, srcPathname, ctx);
680
- if (delResult.status >= 400)
681
- return { status: delResult.status };
682
- return copyResult;
683
- }
684
- // KILL — scheme-polymorphic destroy (plurnk-grammar#203 / 0.28.0). Entry-KILL
685
- // permanently deletes the entry: the canonical delete now, MOVE→/dev/null
686
- // retired from the model's vocabulary. Process-KILL (exec:///) aborts the
687
- // running spawn's controller (the same teardown loop.cancel rides), addressed
688
- // by coordinate pathname (#203). The KILL body is an opaque
689
- // annotation with no runtime meaning; it survives into the log row's tx for
690
- // free via the statement serialization. Status: 200 killed · 404 unknown ·
691
- // 405 log:/// (append-only) · 403 writableBy (the #checkWritable gate, KILL ∈
692
- // MUTATING_OPS) · 200/410/304/404 exec (killed / killed-earlier / exited / unknown) · 501 no-kill/delete scheme.
862
+ if (statement.target === null) {
863
+ return _a.#failure("move-source-required", 400, "MOVE requires a source path.", {}, { retryable: false });
864
+ }
865
+ // MOVE is relocation only - deletion is KILL's job ({§move}, {§move-dev-null-not-special}). The /dev/null
866
+ // and null-body delete-by-MOVE has no alternate meaning.
867
+ if (statement.body === null) {
868
+ return _a.#failure("move-destination-required", 400, "MOVE requires a destination.", {}, {
869
+ recovery: "Use KILL when the intended operation is deletion.",
870
+ retryable: false,
871
+ });
872
+ }
873
+ return this.#moveOrchestration({
874
+ statement,
875
+ source: {
876
+ target: statement.target,
877
+ lineMarker: statement.lineMarker,
878
+ },
879
+ destination: statement.body,
880
+ ctx,
881
+ });
882
+ }
883
+ // KILL is target-polymorphic. Scheme handlers own the optional numeric code's
884
+ // meaning; core retains worker and entry dispatch. {§operation-code-polymorphism}
693
885
  async #handleKill(statement, ctx) {
694
886
  if (statement.op !== "KILL")
695
887
  throw new Error("unreachable");
696
888
  const path = statement.target;
697
- if (path === null)
698
- return { status: 400, error: "KILL requires a target path" };
889
+ if (path === null) {
890
+ return _a.#failure("kill-target-required", 400, "KILL requires a target path.", {}, { retryable: false });
891
+ }
699
892
  const schemeName = schemeNameOf(path);
700
- if (schemeName === null)
701
- return { status: 400, error: "KILL target must be a URL path with a scheme" };
702
- // KILL on log:/// erases the log row(s) — the model's DB-storage curation lever
703
- // (plurnk.md:36, :98), routed to Log.kill below via the killable.kill path. The old
704
- // "append-only" 405 forbade what the grammar requires; FOLD only collapses the render.
893
+ if (schemeName === null) {
894
+ return _a.#failure("kill-target-scheme-required", 400, "KILL target requires a scheme.", {}, { retryable: false });
895
+ }
896
+ // Log targets use the same killable dispatch as streams; erasure is their
897
+ // permanent curation operation. {§model-entry-log-curation}
705
898
  // Process-KILL: any scheme whose handler exposes kill() aborts a live stream — the
706
899
  // exec handler, registered as "exec" + under every runtime tag (sh/node), so a tag-
707
- // addressed stream (sh:///l/t/s) routes here, not to deleteEntry. §exec
900
+ // addressed stream (sh:///l/t/s) routes here, not to deleteEntry. {§exec}
708
901
  const killable = this.#schemes.get(schemeName);
709
902
  if (killable !== undefined && typeof killable.kill === "function") {
710
903
  // Pass the model's OWN scheme so a stream-KILL error answers in the runtime tag the
711
904
  // model addressed (sh:///…), not the internal `exec` ({§fs-answer-in-canon}).
712
905
  const handlerCtx = this.#handlerContext(schemeName, ctx);
713
- return handlerCtx === null ? { status: 501 } : await killable.kill(pathnameFromPath(path), statement.signal, handlerCtx, schemeName);
906
+ if (handlerCtx === null) {
907
+ throw new InvalidOperationResultError(`Registered scheme '${schemeName}' has no dispatch context.`);
908
+ }
909
+ return await killable.kill(entryPathnameOf(path), statement.signal, handlerCtx, schemeName);
714
910
  }
715
911
  if (schemeName === "worker") {
716
- // Entry-path present → KILL a worker-scope scratch ENTRY (delete it), self-only —
717
- // NOT run cancellation. The authority (hostname) names the owner, the pathname the
718
- // entry; only the path-ABSENT form (worker://<name>) terminates the run-as-actor. §worker-scheme
912
+ // Entry-path present → KILL a private owner-held entry (delete it), self-only —
913
+ // NOT worker cancellation. The authority (hostname) names the owner, the pathname the
914
+ // entry; only the path-ABSENT form (worker://<name>) terminates the worker-as-actor. {§worker-scheme}
719
915
  const entryPath = path.kind === "url" ? (path.pathname ?? "") : "";
720
916
  if (entryPath !== "" && entryPath !== "/") {
721
917
  const workerHandler = this.#schemes.get("worker");
722
918
  const handlerCtx = this.#handlerContext("worker", ctx);
723
- return handlerCtx === null ? { status: 501 } : await workerHandler.killEntry(statement, handlerCtx);
919
+ if (handlerCtx === null) {
920
+ throw new InvalidOperationResultError("Registered scheme 'worker' has no dispatch context.");
921
+ }
922
+ return await workerHandler.killEntry(statement, handlerCtx);
724
923
  }
725
- // Terminate an addressed worker's structured scope. `worker://self` = self.
726
- // An idle worker is a no-op 200; a missing worker is 404.
727
- const name = path.kind === "url" ? (path.hostname ?? "") : ""; // §worker-scheme — the worker is the AUTHORITY
728
- if (name === "")
729
- return { status: 400, error: "worker:// kill requires a worker name or ~ (worker://<name>)" };
924
+ const address = WorkerControlAddress.resolve(path, "KILL");
925
+ if (!address.ok)
926
+ return address.result;
927
+ // `~` is the sole current-worker sigil; every other authority is a literal name.
928
+ // An idle worker is a no-op 200; a missing named worker is 404. worker-control-addressing}
929
+ const name = address.authority;
730
930
  let workerId = ctx.workerId;
731
931
  if (name !== "~") {
732
932
  const row = await this.#db.worker_resolve_by_name.get({ workspace_id: ctx.workspaceId, name });
733
- if (row === undefined)
734
- return { status: 404, error: `worker://${name} not found in this workspace` };
933
+ if (row === undefined) {
934
+ return _a.#failure("worker-not-found", 404, `Worker '${name}' does not exist in this workspace.`, {}, { worker: name, retryable: false });
935
+ }
735
936
  workerId = row.id;
736
937
  }
737
938
  if (this.#cancelWorker === undefined)
738
- throw new Error("run kill: cancelWorker capability absent");
739
- // §op-synchronous — KILL is decisive. Await the one lifecycle owner so the
939
+ throw new Error("worker kill: cancelWorker capability absent");
940
+ // {§op-synchronous} — KILL is decisive. Await the one lifecycle owner so the
740
941
  // same-turn pending-work gate observes the complete subtree as terminal.
741
942
  await this.#cancelWorker(workerId, "killed via worker:// KILL");
742
943
  return { status: 200 };
743
944
  }
744
- if (!this.#schemes.has(schemeName))
745
- return { status: 501 };
945
+ if (!this.#schemes.has(schemeName)) {
946
+ return _a.#failure("scheme-not-found", 501, `Scheme '${schemeName}' is not registered.`, {}, { scheme: schemeName, retryable: false });
947
+ }
746
948
  // A host-effecting delete (file) returns 202 to PROPOSE — pass its attrs through so the proposal
747
- // carries the delete target to review (§isProposal fires on 202). Plurnk-internal deletes execute inline.
748
- const delResult = await this.#deleteEntry(schemeName, pathnameFromPath(path), ctx);
749
- return delResult.attrs !== undefined ? { status: delResult.status, attrs: delResult.attrs } : { status: delResult.status };
750
- }
751
- // Multi-file READ fan-out (SPEC §matcher-result "the companion to FIND's survey"). A glob
752
- // READ target resolves to MANY files; READ returns one log row per file that matches, each
753
- // holding that file's matching lines. The matched SET is exactly FIND's survey (which files
754
- // + where — matchLines), so we reuse the scheme's own find, then READ each matched file. One
755
- // model command, N log rows — each row addresses its concrete file, so it folds/kills/re-READs
756
- // on its own. The running sequence counter in runTurn advances by rowsWritten.
757
- // A READ fans out (honors FIND) when it resolves to more than the single exact entry: a glob
758
- // or folder scope, OR a matcher (which selects per-match within whatever the target resolved).
759
- // A bare entry, body-less, is the one direct read. #286
949
+ // carries the delete target to review (#isProposal fires on 202). Plurnk-internal deletes execute inline.
950
+ return this.#deleteEntry(schemeName, entryPathnameOf(path), ctx);
951
+ }
952
+ // A multi-resource READ uses FIND as its resource selector, then writes one
953
+ // READ row per selected resource. Match coordinates remain metadata on that
954
+ // resource; they never create extra rows or replace its content.
760
955
  #readFansOut(statement) {
761
956
  if (statement.op !== "READ")
762
957
  return false;
763
- if ("body" in statement && statement.body !== null)
764
- return true; // a matcher per-match fan-out
958
+ const body = statement.body;
959
+ if (body?.dialect === "semantic" || body?.dialect === "graph")
960
+ return true;
961
+ const schemeName = schemeNameOf(statement.target);
962
+ const handler = schemeName === null
963
+ ? undefined
964
+ : this.#schemes.get(schemeName);
965
+ if (body !== null && typeof handler?.prepareFind === "function")
966
+ return true;
765
967
  const t = statement.target;
766
968
  const p = t === null ? "" : (t.kind === "url" ? t.pathname : t.raw);
767
- if (p.includes("*"))
969
+ if (PathSyntax.hasGlob(p))
768
970
  return true;
769
- const schemeName = schemeNameOf(t);
770
971
  return p.endsWith("/") && schemeName !== null && this.#schemes.manifestFor(schemeName)?.folderScopes === true;
771
972
  }
772
- // Clone the READ onto one concrete match — the FIND already matched, so strip the body
773
- // (no re-match) and set <L> to its readable-row span. A null span (body-less folder/glob
774
- // fan-out) reads the whole entry. #286
775
- static #retargetRead(statement, pathname, span) {
973
+ static #retargetRead(statement, pathname, lineMarker, body) {
776
974
  const t = statement.target;
777
975
  const target = t !== null && t.kind === "url"
778
976
  ? { ...t, pathname, raw: `${t.scheme}://${pathname}` }
779
977
  : { ...t, raw: pathname };
780
- const lineMarker = span !== null ? { marks: [span.rowStart, span.rowEnd] } : null;
781
- return { ...statement, target: target, lineMarker, body: null };
978
+ return { ...statement, target: target, lineMarker, body };
979
+ }
980
+ // READ scope always projects text. Semantic READ reserves a leading decimal
981
+ // for similarity selection; any remaining integers are the text projection.
982
+ // FIND retains its own public result-range interpretation.
983
+ static #readMarkers(statement) {
984
+ const marker = statement.lineMarker;
985
+ if (statement.body?.dialect !== "semantic") {
986
+ return { selection: null, projection: marker };
987
+ }
988
+ if (marker === null)
989
+ return { selection: null, projection: null };
990
+ const [first, ...rest] = marker.marks;
991
+ if (Number.isInteger(first)) {
992
+ return { selection: null, projection: marker };
993
+ }
994
+ return {
995
+ selection: { marks: [first] },
996
+ projection: rest.length === 0 ? null : { marks: rest },
997
+ };
782
998
  }
783
999
  #standardEntryManifest(schemeName, statement) {
784
1000
  const handler = this.#schemes.get(schemeName);
@@ -790,118 +1006,108 @@ class Dispatcher {
790
1006
  const manifest = this.#schemes.manifestFor(schemeName);
791
1007
  return manifest === undefined ? null : { ...manifest, name: target.scheme };
792
1008
  }
793
- static #foldStoredEntryAuthority(statement) {
1009
+ static #storedEntryRead(statement, pathname, lineMarker, body, storage) {
794
1010
  const target = statement.target;
795
1011
  if (target === null || target.kind !== "url")
796
- return statement;
797
- const pathname = foldAuthorityIntoPath(target.hostname, target.pathname);
798
- return {
799
- ...statement,
800
- target: { ...target, hostname: null, pathname, raw: `${target.scheme}://${pathname}` },
801
- };
802
- }
803
- static #storedEntryMatchRead(statement, pathname, span, storage) {
804
- const target = statement.target;
805
- if (target === null || target.kind !== "url")
806
- return _a.#retargetRead(statement, pathname, span);
1012
+ return _a.#retargetRead(statement, pathname, lineMarker, body);
1013
+ if (NetworkAddress.supports(target.scheme)) {
1014
+ const parsed = parsePath(renderAddress(target.scheme, pathname));
1015
+ if (parsed === null || parsed.kind !== "url") {
1016
+ throw new Error(`failed to reconstruct network entry target for ${target.scheme}:${pathname}`);
1017
+ }
1018
+ const reconstructed = { ...parsed, fragment: target.fragment };
1019
+ const raw = renderTarget(reconstructed);
1020
+ if (raw === null)
1021
+ throw new Error(`failed to render network entry target for ${target.scheme}:${pathname}`);
1022
+ return {
1023
+ ...statement,
1024
+ target: { ...reconstructed, raw },
1025
+ lineMarker,
1026
+ body,
1027
+ };
1028
+ }
807
1029
  const hostPrefix = target.hostname === null ? null : `/${target.hostname}`;
808
1030
  const displayPath = hostPrefix !== null && pathname.startsWith(`${hostPrefix}/`)
809
1031
  ? pathname.slice(hostPrefix.length)
810
1032
  : pathname;
811
- const lineMarker = span === null ? null : { marks: [span.rowStart, span.rowEnd] };
812
1033
  return {
813
1034
  ...statement,
814
1035
  target: storage
815
1036
  ? { ...target, hostname: null, pathname, raw: `${target.scheme}://${pathname}` }
816
1037
  : { ...target, pathname: displayPath, raw: `${target.scheme}://${target.hostname ?? ""}${displayPath}` },
817
1038
  lineMarker,
818
- body: null,
1039
+ body,
819
1040
  };
820
1041
  }
821
1042
  async #handleReadFanout(statement, ctx, ids) {
822
1043
  const { workerId, loopId, turnId, sequence, origin, onDispatch } = ids;
823
1044
  const schemeName = schemeNameOf(statement.target);
824
- const findStatement = { ...statement, op: "FIND" };
1045
+ const read = statement;
1046
+ const markers = _a.#readMarkers(read);
1047
+ const findStatement = {
1048
+ ...statement,
1049
+ op: "FIND",
1050
+ lineMarker: markers.selection,
1051
+ };
825
1052
  const standardEntryManifest = schemeName === null ? null : this.#standardEntryManifest(schemeName, statement);
826
- const found = standardEntryManifest === null
827
- ? await this.#run(schemeName, findStatement, ctx)
828
- : await EntryFind.findWorkspaceEntries(_a.#foldStoredEntryAuthority(findStatement), ctx, standardEntryManifest);
1053
+ const found = await this.#run(schemeName, findStatement, ctx);
829
1054
  const matches = found.matches ?? [];
830
- const overflow = typeof found.overflow === "number" ? found.overflow : null;
831
- // §find-count-not-contents applies to WORK as well as rendering. A READ
832
- // over an over-budget selection writes the bounded FIND summary plus one
833
- // loud refusal; it never silently truncates and never materializes a
834
- // hidden prefix/suffix of the set.
835
- if (overflow !== null) {
836
- const findRowId = await this.#writeLog({ statement: { ...statement, op: "FIND" }, result: found, workspaceId: ctx.workspaceId, workerId, loopId, turnId, sequence, origin });
837
- onDispatch?.(findRowId);
838
- const result = {
839
- status: 413,
840
- error: `${overflow} matches exceed the READ materialization budget narrow the target or matcher.`,
841
- };
842
- const readRowId = await this.#writeLog({ statement, result, workspaceId: ctx.workspaceId, workerId, loopId, turnId, sequence: sequence + 1, origin });
1055
+ const omittedItems = typeof found.omittedItems === "number" ? found.omittedItems : null;
1056
+ if (omittedItems !== null) {
1057
+ const maximumItems = typeof found.maximumItems === "number" ? found.maximumItems : null;
1058
+ const result = _a.#failure("read-materialization-too-large", 413, `READ selected ${omittedItems} resources, exceeding the materialization limit${maximumItems === null ? "" : ` of ${maximumItems}`}.`, {}, {
1059
+ resources: omittedItems,
1060
+ ...(maximumItems === null ? {} : { maximumResources: maximumItems }),
1061
+ stage: "materialization",
1062
+ recovery: "Narrow the target or matcher.",
1063
+ retryable: false,
1064
+ });
1065
+ const readRowId = await this.#writeLog({ statement, result, workspaceId: ctx.workspaceId, workerId, loopId, turnId, sequence, origin });
843
1066
  onDispatch?.(readRowId);
844
- return { ...result, rowsWritten: 2 };
1067
+ return { ...result, rowsWritten: 1 };
845
1068
  }
846
1069
  // Find-less scheme, a matcher/scope error, or zero matches → a single row carrying the
847
1070
  // status, exactly like a non-fanned READ. The model sees the empty/failed result, not silence.
848
1071
  if (found.status !== 200 || matches.length === 0) {
849
- const result = { status: found.status === 200 ? 204 : found.status };
1072
+ const result = found.status === 200
1073
+ ? { status: 204, ...(read.body === null ? {} : { matches: [] }) }
1074
+ : found;
850
1075
  const id = await this.#writeLog({ statement, result, workspaceId: ctx.workspaceId, workerId, loopId, turnId, sequence, origin });
851
1076
  onDispatch?.(id);
852
1077
  return { ...result, rowsWritten: 1 };
853
1078
  }
854
- // One READ row per MATCH — deliver its source lines (or the whole entry for a body-less
855
- // folder/glob) while the logged statement carries the corresponding readable-row scope.
856
- // The delivery uses a raw line-slice rather than reinterpreting provenance as the scheme's
857
- // structural <L>. Read each distinct entry's content once, then line-slice per match. #286
858
- // §matcher-selection-signal — the SELECTION SUMMARY row: the internal FIND that located
859
- // the matches is WRITTEN (op=FIND, its full result — per-hit matchSpan + matchPath, the
860
- // canonical dialect coordinate) before the per-match deliveries, exactly as if the model
861
- // had FINDed then READ. On a degenerate single-line document the N delivery rows are
862
- // identical whole-file lines; the summary row is what tells the model its query hit N
863
- // times and WHERE (run30: two hits indistinguishable from failure; 17 retries, 508).
864
- const findRowId = await this.#writeLog({ statement: { ...statement, op: "FIND" }, result: found, workspaceId: ctx.workspaceId, workerId, loopId, turnId, sequence, origin });
865
- onDispatch?.(findRowId);
866
- const wholeByPath = new Map();
867
1079
  const fannedStatuses = [];
868
- let written = 1;
869
- for (const m of matches) {
1080
+ const scopeNormalizations = [];
1081
+ let written = 0;
1082
+ for (const match of matches) {
870
1083
  ctx.signal?.throwIfAborted();
871
- let whole = wholeByPath.get(m.pathname);
872
- if (whole === undefined) {
873
- const retargeted = standardEntryManifest === null
874
- ? _a.#retargetRead(statement, m.pathname, null)
875
- : _a.#storedEntryMatchRead(statement, m.pathname, null, true);
876
- whole = standardEntryManifest === null
877
- ? await this.#run(schemeName, retargeted, ctx)
878
- : await EntryOps.readWorkspaceEntry(retargeted, ctx, standardEntryManifest);
879
- wholeByPath.set(m.pathname, whole);
880
- }
881
- const result = _a.#sliceMatch(whole, m.span);
882
- const deliveredStatement = standardEntryManifest === null
883
- ? _a.#retargetRead(statement, m.pathname, m.span)
884
- : _a.#storedEntryMatchRead(statement, m.pathname, m.span, false);
885
- const id = await this.#writeLog({ statement: deliveredStatement, result, workspaceId: ctx.workspaceId, workerId, loopId, turnId, sequence: sequence + written, origin });
1084
+ const executionStatement = standardEntryManifest === null
1085
+ ? _a.#retargetRead(statement, match.pathname, markers.projection, null)
1086
+ : _a.#storedEntryRead(statement, match.pathname, markers.projection, null, true);
1087
+ const readResult = standardEntryManifest === null
1088
+ ? await this.#run(schemeName, executionStatement, ctx)
1089
+ : await EntryOps.readWorkspaceEntry(executionStatement, ctx, standardEntryManifest);
1090
+ const result = read.body === null
1091
+ ? readResult
1092
+ : { ...readResult, matches: match.matches };
1093
+ const loggedStatement = standardEntryManifest === null
1094
+ ? _a.#retargetRead(statement, match.pathname, statement.lineMarker, read.body)
1095
+ : _a.#storedEntryRead(statement, match.pathname, statement.lineMarker, read.body, false);
1096
+ const id = await this.#writeLog({ statement: loggedStatement, result, workspaceId: ctx.workspaceId, workerId, loopId, turnId, sequence: sequence + written, origin });
886
1097
  onDispatch?.(id);
887
1098
  fannedStatuses.push(result.status);
1099
+ scopeNormalizations.push(...(result.scopeNormalizations ?? []));
888
1100
  written++;
889
1101
  }
890
- return { status: 200, rowsWritten: written, fannedStatuses };
891
- }
892
- // Deliver one match: the whole entry (body-less, span null) or the source lines at the span —
893
- // a RAW line-slice, so a structural mimetype (json item-index / xml) doesn't mis-slice a span
894
- // that is, by construction, source line numbers (#286).
895
- static #sliceMatch(whole, span) {
896
- if (whole.status !== 200 || span === null)
897
- return whole;
898
- const sliced = LineMarkerOps.sliceLines(typeof whole.content === "string" ? whole.content : "", { marks: [span.lineStart, span.lineEnd] });
899
- if (sliced.status !== 200)
900
- return { status: sliced.status, error: sliced.error };
901
- return { status: 200, content: sliced.text ?? "", mimetype: "text/markdown", startLine: sliced.startLine ?? span.lineStart };
902
- }
903
- // §model-entry — mirror a verbatim model emission back as an actionless `model` log row, so
904
- // the model can finally SEE its own prior output (and reason through its own syntax errors).
1102
+ return {
1103
+ status: 200,
1104
+ rowsWritten: written,
1105
+ fannedStatuses,
1106
+ ...(scopeNormalizations.length === 0 ? {} : { scopeNormalizations }),
1107
+ };
1108
+ }
1109
+ // {§model-entry} — mirror an admitted model emission back as an actionless `model` log row, so
1110
+ // the model can inspect and curate its own prior behavior.
905
1111
  // Born FOLDED by default (budget-neutral until OPENed); the turn-0 exemplar passes folded:false
906
1112
  // (born open — the one worked example the model orients on, thinning the grammar). text/vnd.plurnk.
907
1113
  async writeModelEntry({ verbatim, workerId, loopId, turnId, sequence, folded, origin = "model", reasoningItems }) {
@@ -909,7 +1115,7 @@ class Dispatcher {
909
1115
  worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence,
910
1116
  origin, source: null, op: "model", suffix: "", signal: null,
911
1117
  scheme: null, username: null, password: null, hostname: null, port: null,
912
- pathname: null, params: null, fragment: null, lineMarker: null,
1118
+ pathname: null, query: null, fragment: null, lineMarker: null,
913
1119
  tx: "", mimetype_tx: "text/vnd.plurnk",
914
1120
  rx: JSON.stringify({ content: verbatim, mimetype: "text/vnd.plurnk" }),
915
1121
  mimetype_rx: "application/json",
@@ -922,7 +1128,7 @@ class Dispatcher {
922
1128
  await this.#db.engine_fold_log_entry.run({ id: row.id });
923
1129
  return row.id;
924
1130
  }
925
- // PLAN — the model's reasoning op (the 11th op). An ordinary op: dispatched like any
1131
+ // PLAN — the model's intended-goals op. An ordinary op: dispatched like any
926
1132
  // other, logged, and broadcast to the client as a log entry — but a pure no-op for
927
1133
  // state (PLAN ∉ MUTATING_OPS); its body serializes into the log row's tx, no effect.
928
1134
  #handlePlan(statement) {
@@ -930,93 +1136,708 @@ class Dispatcher {
930
1136
  throw new Error("unreachable");
931
1137
  return { status: 200 };
932
1138
  }
933
- // Same- and cross-scheme COPY share one orchestrator — §copy-cross-scheme-copy §move-cross-scheme-move
934
- async #copyOrchestration({ statement, srcPath, dstPath, ctx }) {
935
- const srcSchemeName = schemeNameOf(srcPath);
936
- const dstSchemeName = schemeNameOf(dstPath);
937
- if (srcSchemeName === null || dstSchemeName === null)
938
- return { status: 400, error: "COPY/MOVE require URL paths with schemes" };
939
- // {§worker-authority-carving} the entry-copy seam is pathname-keyed; on worker:// it
940
- // addresses the COMMONS. A space's content moves via READ + EDIT, not COPY.
941
- for (const p of [srcPath, dstPath]) {
942
- if (p.kind === "url" && p.scheme === "worker" && (p.hostname ?? "") !== "") {
943
- return { status: 400, error: "COPY/MOVE address the commons (worker:///…); a space's content moves via READ + EDIT" };
944
- }
1139
+ static #isDispatchResult(value) {
1140
+ return "status" in value;
1141
+ }
1142
+ async #resolveResourceSelection(selection, ctx) {
1143
+ const { target, lineMarker } = selection;
1144
+ const scheme = schemeNameOf(target);
1145
+ if (scheme === null) {
1146
+ return _a.#failure("resource-scheme-required", 400, "COPY and MOVE resources require a scheme.", {}, { retryable: false });
1147
+ }
1148
+ if (target.kind === "url" && target.scheme === "worker" && (target.hostname ?? "") !== "") {
1149
+ return _a.#failure("worker-copy-address-invalid", 400, "COPY and MOVE do not address named worker spaces.", {}, {
1150
+ recovery: "Move worker-space content with READ and EDIT.",
1151
+ retryable: false,
1152
+ });
1153
+ }
1154
+ const handler = this.#schemes.get(scheme);
1155
+ const manifest = this.#schemes.manifestFor(scheme);
1156
+ if (handler === undefined || manifest === undefined) {
1157
+ return _a.#failure("scheme-not-found", 501, `COPY or MOVE addressed the unregistered scheme '${scheme}'.`, {}, {
1158
+ scheme,
1159
+ retryable: false,
1160
+ });
1161
+ }
1162
+ if (manifest.category !== "data") {
1163
+ return _a.#failure("entry-operation-unsupported", 400, `COPY and MOVE require entry-bearing resources; '${scheme}' is a ${manifest.category} scheme.`, {}, {
1164
+ scheme,
1165
+ category: manifest.category,
1166
+ retryable: false,
1167
+ });
1168
+ }
1169
+ const fragment = target.kind === "url" ? target.fragment : null;
1170
+ const channel = fragment ?? manifest.defaultChannel;
1171
+ if (channel.length === 0) {
1172
+ return _a.#failure("channel-required", 400, `The '${scheme}' scheme has no default channel.`, {}, {
1173
+ scheme,
1174
+ recovery: "Address a named channel with a URI fragment.",
1175
+ retryable: false,
1176
+ });
1177
+ }
1178
+ if (fragment !== null
1179
+ && fragment !== manifest.defaultChannel
1180
+ && !Object.hasOwn(manifest.channels, fragment)) {
1181
+ const availableChannels = [
1182
+ ...new Set([manifest.defaultChannel, ...Object.keys(manifest.channels)]),
1183
+ ].filter((candidate) => candidate.length > 0);
1184
+ return _a.#failure("channel-not-found", 400, `Channel #${fragment} is not declared by the '${scheme}' scheme.`, {}, {
1185
+ requestedChannel: fragment,
1186
+ availableChannels,
1187
+ retryable: false,
1188
+ });
1189
+ }
1190
+ const pathname = entryPathnameOf(target);
1191
+ const canonicalFilePath = scheme === "file"
1192
+ ? Namespace.canonicalizeSpelling(pathname, await this.#workspaceRoot(ctx.workspaceId))
1193
+ : pathname;
1194
+ return {
1195
+ target,
1196
+ lineMarker,
1197
+ scheme,
1198
+ pathname,
1199
+ identityPathname: canonicalFilePath ?? pathname,
1200
+ channel,
1201
+ manifest,
1202
+ };
1203
+ }
1204
+ async #selectSource(selection, ctx) {
1205
+ const read = await this.#readEntry(selection.scheme, selection.pathname, ctx);
1206
+ if (read.status >= 400)
1207
+ return read;
1208
+ if (read.status !== 200 || read.entry === null) {
1209
+ throw new InvalidOperationResultError(`The '${selection.scheme}' scheme returned status ${read.status} without a COPY/MOVE source entry.`);
1210
+ }
1211
+ const selected = read.entry.channels[selection.channel];
1212
+ if (selected === undefined) {
1213
+ return _a.#failure("channel-not-found", 404, `No channel named #${selection.channel} exists at ${renderAddress(selection.scheme, selection.identityPathname)}.`, {}, {
1214
+ target: renderAddress(selection.scheme, selection.identityPathname),
1215
+ requestedChannel: selection.channel,
1216
+ availableChannels: Object.keys(read.entry.channels),
1217
+ retryable: false,
1218
+ });
1219
+ }
1220
+ let content = selected.content;
1221
+ let scopeNormalizations;
1222
+ if (await MimetypeBinary.isBinaryMimetype(selected.mimetype, ctx.mimetypes)) {
1223
+ return _a.#failure("binary-source-unsupported", 415, `Channel #${selection.channel} is a binary marker and cannot be copied or moved.`, {}, {
1224
+ channel: selection.channel,
1225
+ mimetype: selected.mimetype,
1226
+ recovery: "Use a source with a readable text projection.",
1227
+ retryable: false,
1228
+ });
1229
+ }
1230
+ if (selection.lineMarker !== null) {
1231
+ const sliced = LineMarkerOps.sliceLinesRaw(content, selection.lineMarker);
1232
+ if (sliced.status !== 200)
1233
+ return Results.assert(sliced);
1234
+ content = sliced.text ?? "";
1235
+ scopeNormalizations = sliced.scopeNormalizations;
1236
+ }
1237
+ return {
1238
+ ...selection,
1239
+ content,
1240
+ mimetype: selected.mimetype,
1241
+ ...(scopeNormalizations === undefined ? {} : { scopeNormalizations }),
1242
+ };
1243
+ }
1244
+ static #prependScopeNormalizations(result, scopeNormalizations) {
1245
+ if (scopeNormalizations === undefined)
1246
+ return result;
1247
+ return Results.assert({
1248
+ ...result,
1249
+ scopeNormalizations: [
1250
+ ...scopeNormalizations,
1251
+ ...(result.scopeNormalizations ?? []),
1252
+ ],
1253
+ });
1254
+ }
1255
+ #resourceAddress(selection) {
1256
+ const address = renderTarget({
1257
+ scheme: selection.scheme === "file" ? null : selection.scheme,
1258
+ pathname: selection.scheme === "file"
1259
+ ? selection.identityPathname.replace(/^\//, "")
1260
+ : selection.identityPathname,
1261
+ fragment: selection.channel === selection.manifest.defaultChannel
1262
+ ? null
1263
+ : selection.channel,
1264
+ });
1265
+ if (address === null)
1266
+ throw new Error("resolved resource selection has no renderable address");
1267
+ return address;
1268
+ }
1269
+ #pendingEffect(selection, action) {
1270
+ return {
1271
+ target: this.#resourceAddress(selection),
1272
+ action,
1273
+ };
1274
+ }
1275
+ static #appliedEffects(result, pending) {
1276
+ // {§edit-result-copy-move-effects} — only an applied mutation earns
1277
+ // engine-composed effects; native scheme receipts remain internal input.
1278
+ const exact = Results.assert(result);
1279
+ if (exact.status === 304 || exact.status === 202 || exact.status >= 300)
1280
+ return exact;
1281
+ if (exact.effects !== undefined) {
1282
+ throw new InvalidOperationResultError("A COPY/MOVE mutation result supplied effects before engine composition.");
1283
+ }
1284
+ const batch = exact.editReceipt === undefined
1285
+ ? undefined
1286
+ : assertEditBatchReceipt(exact.editReceipt);
1287
+ const single = batch === undefined && exact.receipt !== undefined
1288
+ ? assertEditReceipt(exact.receipt)
1289
+ : undefined;
1290
+ const batchSize = batch === undefined
1291
+ ? undefined
1292
+ : "disposition" in batch
1293
+ ? batch.superseded.length
1294
+ : batch.effects.length;
1295
+ if (batchSize !== undefined && batchSize !== pending.length) {
1296
+ throw new InvalidOperationResultError(`COPY/MOVE expected ${pending.length} receipt projections, got ${batchSize}.`);
945
1297
  }
946
- const srcHandler = this.#schemes.get(srcSchemeName);
947
- const dstHandler = this.#schemes.get(dstSchemeName);
948
- if (srcHandler === undefined || dstHandler === undefined)
949
- return { status: 501 };
950
- const srcPathname = pathnameFromPath(srcPath);
951
- const dstPathname = pathnameFromPath(dstPath);
952
- const srcResult = await this.#readEntry(srcSchemeName, srcPathname, ctx);
953
- if (srcResult.status !== 200 || srcResult.entry === null)
954
- return { status: 404, error: `COPY/MOVE source not found: ${srcSchemeName}://${srcPathname}` }; // §copy-missing-source-404 §move-missing-source-404
955
- const entry = srcResult.entry;
956
- // Destination read the conflict/no-op verdict is deferred until the
957
- // to-be-written content is known (after <L> slice + tag resolution below),
958
- // so an identical re-copy resolves to 304 instead of a phantom 409.
959
- const dstExisting = await this.#readEntry(dstSchemeName, dstPathname, ctx);
960
- // Mimetype compatibility check against the destination scheme's manifest
961
- const dstManifest = dstHandler.constructor.manifest;
962
- const dstChannels = dstManifest?.channels ?? {};
963
- for (const [channelName, channelData] of Object.entries(entry.channels)) {
964
- const expectedMimetype = dstChannels[channelName];
965
- if (expectedMimetype !== undefined && expectedMimetype !== channelData.mimetype) {
966
- return { status: 415, error: `mimetype mismatch on channel '${channelName}': ${channelData.mimetype} vs ${expectedMimetype}` }; // cross-mimetype COPY/MOVE → 415, never coerce — §channel-mimetype-cross-mimetype-415
1298
+ if (single !== undefined && pending.length !== 1) {
1299
+ throw new InvalidOperationResultError(`COPY/MOVE received one receipt for ${pending.length} resource effects.`);
1300
+ }
1301
+ let effects;
1302
+ if (batch !== undefined && "disposition" in batch) {
1303
+ const replacement = pending[0];
1304
+ if (replacement === undefined) {
1305
+ throw new InvalidOperationResultError("A reviewer-replaced COPY/MOVE batch has no resource effect.");
1306
+ }
1307
+ if (pending.some(({ target, action }) => target !== replacement.target || action !== replacement.action)) {
1308
+ throw new InvalidOperationResultError("A reviewer-replaced COPY/MOVE batch spans incompatible resource effects.");
967
1309
  }
1310
+ effects = [{
1311
+ ...replacement,
1312
+ receipt: projectEditReceipt(batch, 0),
1313
+ }];
968
1314
  }
969
- // `<L>` source range slicing per SPEC.md §op-invariants (symmetric with READ
970
- // `<L>` — source range, no line-number prefix).
971
- // Applied to every channel of the source entry. Binary channels return
972
- // 415 since line semantics don't apply.
973
- const lineMarker = statement.lineMarker ?? null;
974
- let channels = entry.channels;
975
- if (lineMarker !== null) {
976
- const sliced = {};
977
- for (const [channelName, channelData] of Object.entries(entry.channels)) {
978
- if (MimetypeBinary.isBinaryMimetype(channelData.mimetype)) {
979
- return { status: 415, error: `cannot slice <L> on binary channel '${channelName}' (${channelData.mimetype})` };
1315
+ else {
1316
+ effects = pending.map((effect, index) => ({
1317
+ ...effect,
1318
+ ...(batch !== undefined
1319
+ ? { receipt: projectEditReceipt(batch, index) }
1320
+ : single !== undefined
1321
+ ? { receipt: single }
1322
+ : {}),
1323
+ }));
1324
+ }
1325
+ assertResourceEffects(effects);
1326
+ const { editReceipt: _editReceipt, receipt: _receipt, ...withoutInternalReceipts } = exact;
1327
+ return {
1328
+ ...withoutInternalReceipts,
1329
+ effects,
1330
+ };
1331
+ }
1332
+ // {§copy-move-observation} {§edit-result-copy-move-effects} — scoped
1333
+ // COPY/MOVE into an unscoped channel is a text materialization even when
1334
+ // CRUD creates the channel wholesale. Carry the receipt through synchronous
1335
+ // writes and proposals; the destination scheme owns reviewed output.
1336
+ static #withEditMaterialization(result, receipt) {
1337
+ const exact = Results.assert(result);
1338
+ if (exact.status !== 200 && exact.status !== 201 && exact.status !== 202)
1339
+ return exact;
1340
+ const materialized = exact.editReceipt === undefined
1341
+ ? assertEditBatchReceipt(receipt)
1342
+ : assertEditBatchReceipt(exact.editReceipt);
1343
+ return {
1344
+ ...exact,
1345
+ editReceipt: materialized,
1346
+ ...(exact.status === 202
1347
+ ? {
1348
+ attrs: {
1349
+ ...exact.attrs,
1350
+ editReceipt: materialized,
1351
+ },
980
1352
  }
981
- const r = LineMarkerOps.sliceLinesRaw(channelData.content ?? "", lineMarker);
982
- if (r.status !== 200)
983
- return { status: r.status, error: r.error };
984
- sliced[channelName] = { ...channelData, content: r.text ?? "" };
1353
+ : {}),
1354
+ };
1355
+ }
1356
+ #finalizeEffects(result, selection, pending) {
1357
+ const routed = this.#withProposalRoute(result, selection);
1358
+ if (routed.status !== 202)
1359
+ return _a.#appliedEffects(routed, pending);
1360
+ return {
1361
+ ...routed,
1362
+ attrs: {
1363
+ ...routed.attrs,
1364
+ proposalEffects: pending,
1365
+ },
1366
+ };
1367
+ }
1368
+ static #effectsOf(result) {
1369
+ return result.effects === undefined
1370
+ ? []
1371
+ : assertResourceEffects(result.effects);
1372
+ }
1373
+ static #withCombinedEffects(result, ...additional) {
1374
+ const existing = _a.#effectsOf(result);
1375
+ const effects = [...existing, ...additional.flat()];
1376
+ const { effects: _effects, ...withoutEffects } = result;
1377
+ return effects.length === 0
1378
+ ? withoutEffects
1379
+ : { ...withoutEffects, effects: assertResourceEffects(effects) };
1380
+ }
1381
+ static #settleProposalEffects(original, settlement) {
1382
+ const pending = original.attrs
1383
+ ?.proposalEffects;
1384
+ if (pending === undefined
1385
+ || settlement.resolution.decision !== "accept"
1386
+ || settlement.applied === undefined
1387
+ || settlement.applied.status >= 300) {
1388
+ return settlement;
1389
+ }
1390
+ const projected = settlement.resolution.result ?? {};
1391
+ const aggregate = settlement.applied.editReceipt;
1392
+ const applied = _a.#appliedEffects({
1393
+ ...projected,
1394
+ status: settlement.applied.status,
1395
+ ...(aggregate === undefined ? {} : { editReceipt: aggregate }),
1396
+ }, pending);
1397
+ const { status: _status, body: _body, ...result } = applied;
1398
+ const { body: _resolutionBody, ...resolution } = settlement.resolution;
1399
+ return {
1400
+ ...settlement,
1401
+ resolution: {
1402
+ ...resolution,
1403
+ result,
1404
+ },
1405
+ };
1406
+ }
1407
+ #recordEditSettlement(statement, settlement) {
1408
+ if (statement.op !== "EDIT")
1409
+ return;
1410
+ const prepared = this.#preparedEdits.get(statement);
1411
+ if (prepared === undefined || !prepared.first) {
1412
+ throw new InvalidOperationResultError("An EDIT proposal settled without its prepared batch owner.");
1413
+ }
1414
+ const { resolution, applied } = settlement;
1415
+ if (resolution.decision !== "accept"
1416
+ || applied === undefined
1417
+ || applied.status >= 300) {
1418
+ prepared.batch.aggregate = undefined;
1419
+ return;
1420
+ }
1421
+ if (applied.editReceipt === null) {
1422
+ prepared.batch.aggregate = undefined;
1423
+ return;
1424
+ }
1425
+ if (applied.editReceipt !== undefined) {
1426
+ prepared.batch.aggregate = assertEditBatchReceipt(applied.editReceipt);
1427
+ return;
1428
+ }
1429
+ if (resolution.body !== undefined)
1430
+ prepared.batch.aggregate = undefined;
1431
+ }
1432
+ static #settlementEffects(settlement) {
1433
+ const effects = settlement.resolution.result
1434
+ ?.effects;
1435
+ return effects === undefined ? [] : assertResourceEffects(effects);
1436
+ }
1437
+ static #withSettlementEffects(settlement, effects) {
1438
+ const projected = (settlement.resolution.result ?? {});
1439
+ const { effects: _effects, ...withoutEffects } = projected;
1440
+ const result = effects.length === 0
1441
+ ? withoutEffects
1442
+ : {
1443
+ ...withoutEffects,
1444
+ effects: assertResourceEffects(effects),
1445
+ };
1446
+ return {
1447
+ ...settlement,
1448
+ resolution: {
1449
+ ...settlement.resolution,
1450
+ result,
1451
+ },
1452
+ };
1453
+ }
1454
+ #sameChannel(left, right) {
1455
+ return left.scheme === right.scheme
1456
+ && left.identityPathname === right.identityPathname
1457
+ && left.channel === right.channel;
1458
+ }
1459
+ #withProposalRoute(result, selection) {
1460
+ if (result.status !== 202)
1461
+ return result;
1462
+ return {
1463
+ ...result,
1464
+ attrs: {
1465
+ ...result.attrs,
1466
+ proposalScheme: selection.scheme,
1467
+ proposalTarget: {
1468
+ scheme: selection.scheme,
1469
+ pathname: selection.identityPathname,
1470
+ },
1471
+ },
1472
+ };
1473
+ }
1474
+ async #invokeEditBatch(selection, edits, ctx) {
1475
+ const handler = this.#schemes.get(selection.scheme);
1476
+ if (typeof handler?.editBatch !== "function") {
1477
+ return _a.#failure("operation-not-implemented", 501, `Scheme '${selection.scheme}' does not implement EDIT batches.`, {}, {
1478
+ scheme: selection.scheme,
1479
+ operation: "EDIT",
1480
+ retryable: false,
1481
+ });
1482
+ }
1483
+ const statements = edits.map(({ marker, body, tags, position }) => ({
1484
+ op: "EDIT",
1485
+ suffix: "",
1486
+ signal: tags,
1487
+ target: selection.target,
1488
+ lineMarker: marker,
1489
+ body,
1490
+ position,
1491
+ }));
1492
+ const addressedScheme = selection.target.kind === "url"
1493
+ ? selection.target.scheme
1494
+ : selection.scheme;
1495
+ try {
1496
+ const result = Results.assert(await handler.editBatch(statements, new SchemeCtxImpl(ctx, addressedScheme, selection.manifest, this.#liveSubscriptions)));
1497
+ return this.#withProposalRoute(result, selection);
1498
+ }
1499
+ catch (err) {
1500
+ if (err instanceof InvalidOperationResultError)
1501
+ throw err;
1502
+ console.error(`Scheme '${selection.scheme}' COPY/MOVE edit threw outside its operation result contract:`, err);
1503
+ return _a.#failure("scheme-handler-threw", 500, `The '${selection.scheme}' scheme did not produce a COPY/MOVE edit result.`, {}, {
1504
+ stage: "scheme-dispatch",
1505
+ scheme: selection.scheme,
1506
+ operation: "EDIT",
1507
+ });
1508
+ }
1509
+ }
1510
+ async #writeDestination(statement, source, destination, ctx) {
1511
+ const existingResult = await this.#readEntry(destination.scheme, destination.pathname, ctx);
1512
+ if (existingResult.status >= 400 && existingResult.status !== 404) {
1513
+ return existingResult;
1514
+ }
1515
+ const existing = existingResult.status === 200
1516
+ ? existingResult.entry
1517
+ : null;
1518
+ if (existingResult.status === 200 && existing === null) {
1519
+ throw new InvalidOperationResultError(`The '${destination.scheme}' scheme returned 200 without a destination entry.`);
1520
+ }
1521
+ const destinationChannel = existing?.channels[destination.channel];
1522
+ const expectedMimetype = destinationChannel?.mimetype
1523
+ ?? await PathMimetype.resolveEntryMimetype(destination.pathname, destination.manifest.channels[destination.channel] ?? source.mimetype, ctx.mimetypes);
1524
+ if (source.mimetype !== expectedMimetype) {
1525
+ return _a.#failure("mimetype-mismatch", 415, `COPY or MOVE cannot write '${source.mimetype}' into a '${expectedMimetype}' channel.`, {}, {
1526
+ channel: destination.channel,
1527
+ sourceMimetype: source.mimetype,
1528
+ destinationMimetype: expectedMimetype,
1529
+ retryable: false,
1530
+ });
1531
+ }
1532
+ const tags = Array.isArray(statement.signal) ? statement.signal : [];
1533
+ const destinationEffect = this.#pendingEffect(destination, destinationChannel === undefined ? "create" : "update");
1534
+ if (destination.lineMarker !== null) {
1535
+ if (destinationChannel === undefined) {
1536
+ return _a.#failure("destination-region-not-found", 404, `A destination region requires an existing #${destination.channel} channel.`, {}, {
1537
+ destination: this.#resourceAddress(destination),
1538
+ retryable: false,
1539
+ });
1540
+ }
1541
+ if (await MimetypeBinary.isBinaryMimetype(destinationChannel.mimetype, ctx.mimetypes)) {
1542
+ return _a.#failure("binary-region-unsupported", 415, `Channel #${destination.channel} is binary and cannot receive a textual region.`, {}, {
1543
+ channel: destination.channel,
1544
+ mimetype: destinationChannel.mimetype,
1545
+ retryable: false,
1546
+ });
985
1547
  }
986
- channels = sliced;
987
- }
988
- // Tag resolution: signal = replace (§copy-signal-replaces-source-tags); absent/empty = carry from source (§copy-no-signal-carries-source-tags)
989
- const tags = (Array.isArray(statement.signal) && statement.signal.length > 0)
990
- ? statement.signal
991
- : entry.tags;
992
- // 304/409 on an existing destination (SPEC §copy): a re-copy that would write
993
- // exactly what's already there — same channel contents, same tags — is a no-op
994
- // (304), mirroring EDIT's 304-on-noop (§edit). A divergent destination is a real
995
- // collision (409); COPY/MOVE never clobbers.
996
- if (dstExisting !== null && dstExisting.status === 200 && dstExisting.entry !== null) {
997
- const dstChannels = dstExisting.entry.channels;
998
- const writeNames = Object.keys(channels).sort();
999
- const dstNames = Object.keys(dstChannels).sort();
1000
- const sameContent = writeNames.length === dstNames.length
1001
- && writeNames.every((n, i) => n === dstNames[i] && (channels[n]?.content ?? "") === (dstChannels[n]?.content ?? ""));
1002
- const sameTags = [...tags].sort().join("") === [...dstExisting.entry.tags].sort().join("");
1003
- if (sameContent && sameTags)
1004
- return { status: 304 }; // identical → §copy-noop-304
1005
- return { status: 409, error: `COPY/MOVE destination exists: ${dstSchemeName}://${dstPathname}` }; // §copy-conflict-409
1006
- }
1007
- const writeResult = await this.#writeEntry(dstSchemeName, dstPathname, { channels, tags }, ctx);
1008
- // A file dest returns 202 (disk write → §membership review): propagate the
1009
- // proposal so dispatch runs the gate + routes applyResolution to the dest.
1010
- if (writeResult.status === 202)
1011
- return { status: 202, attrs: writeResult.attrs, body: writeResult.body };
1012
- return { status: writeResult.status, entryId: writeResult.entryId, created: writeResult.created };
1013
- }
1014
- // §send-premature-terminate the unified PENDING SET, judged at the terminal's OWN dispatch
1548
+ const edited = await this.#invokeEditBatch(destination, [{
1549
+ marker: destination.lineMarker,
1550
+ body: source.content,
1551
+ tags,
1552
+ position: statement.position,
1553
+ }], ctx);
1554
+ return this.#finalizeEffects(edited, destination, [destinationEffect]);
1555
+ }
1556
+ if (destinationChannel !== undefined
1557
+ && destinationChannel.content !== source.content) {
1558
+ return _a.#failure("copy-destination-exists", 409, `COPY or MOVE destination ${this.#resourceAddress(destination)} already contains different content.`, {}, {
1559
+ destination: this.#resourceAddress(destination),
1560
+ retryable: false,
1561
+ });
1562
+ }
1563
+ const priorTags = existing?.tags ?? [];
1564
+ const mergedTags = [...new Set([...priorTags, ...tags])];
1565
+ const addsTags = mergedTags.length !== priorTags.length;
1566
+ if (destinationChannel !== undefined && !addsTags)
1567
+ return { status: 304 };
1568
+ const channels = {
1569
+ ...(existing?.channels ?? {}),
1570
+ [destination.channel]: {
1571
+ content: source.content,
1572
+ mimetype: source.mimetype,
1573
+ },
1574
+ };
1575
+ const written = await this.#writeEntry(destination.scheme, destination.pathname, { channels, tags: mergedTags }, ctx);
1576
+ const exactWritten = Results.assert(written);
1577
+ const materialized = source.lineMarker === null
1578
+ || (exactWritten.status !== 200 && exactWritten.status !== 201 && exactWritten.status !== 202)
1579
+ ? exactWritten
1580
+ : _a.#withEditMaterialization(exactWritten, editReceipt(destinationChannel?.content ?? "", source.content, [{
1581
+ marker: { marks: [1, -1] },
1582
+ body: source.content,
1583
+ }]));
1584
+ return this.#finalizeEffects(materialized, destination, [destinationEffect]);
1585
+ }
1586
+ async #copyOrchestration({ statement, source, destination, ctx, }) {
1587
+ const resolvedSource = await this.#resolveResourceSelection(source, ctx);
1588
+ if (_a.#isDispatchResult(resolvedSource))
1589
+ return resolvedSource;
1590
+ const resolvedDestination = await this.#resolveResourceSelection(destination, ctx);
1591
+ if (_a.#isDispatchResult(resolvedDestination))
1592
+ return resolvedDestination;
1593
+ const selected = await this.#selectSource(resolvedSource, ctx);
1594
+ if (_a.#isDispatchResult(selected))
1595
+ return selected;
1596
+ const result = await this.#writeDestination(statement, selected, resolvedDestination, ctx);
1597
+ return _a.#prependScopeNormalizations(result, selected.scopeNormalizations);
1598
+ }
1599
+ #deferredMoveSource(source, destination) {
1600
+ return {
1601
+ target: source.target,
1602
+ lineMarker: source.lineMarker,
1603
+ scheme: source.scheme,
1604
+ pathname: source.pathname,
1605
+ channel: source.channel,
1606
+ destination: this.#resourceAddress(destination),
1607
+ };
1608
+ }
1609
+ #moveFailureAfterDestination(result, destination, destinationEffects) {
1610
+ const exact = Results.assert(result);
1611
+ if (exact.status < 400) {
1612
+ throw new InvalidOperationResultError("A successful MOVE source result was classified as a partial failure.");
1613
+ }
1614
+ if (exact.problem === undefined) {
1615
+ throw new InvalidOperationResultError("A failed MOVE source mutation has no Problem Details.");
1616
+ }
1617
+ const failed = Results.assert({
1618
+ ...exact,
1619
+ problem: {
1620
+ ...exact.problem,
1621
+ operation: "MOVE",
1622
+ destinationWritten: true,
1623
+ destination,
1624
+ },
1625
+ });
1626
+ return _a.#withCombinedEffects(failed, destinationEffects);
1627
+ }
1628
+ async #removeMoveSource(statement, source, ctx) {
1629
+ const effect = this.#pendingEffect(source, source.lineMarker === null ? "delete" : "update");
1630
+ if (source.lineMarker === null) {
1631
+ const deleted = await this.#deleteChannel(source.scheme, source.pathname, source.channel, ctx);
1632
+ return this.#finalizeEffects(Results.assert(deleted), source, [effect]);
1633
+ }
1634
+ const edited = await this.#invokeEditBatch(source, [{
1635
+ marker: source.lineMarker,
1636
+ body: "",
1637
+ tags: null,
1638
+ position: statement.position,
1639
+ }], ctx);
1640
+ return this.#finalizeEffects(edited, source, [effect]);
1641
+ }
1642
+ async #moveWithinChannel(statement, source, destination, ctx) {
1643
+ if (source.lineMarker === null) {
1644
+ if (destination.lineMarker !== null) {
1645
+ return _a.#failure("move-region-overlap", 409, "MOVE cannot insert a whole channel into itself and then remove that channel.", {}, {
1646
+ source: this.#resourceAddress(source),
1647
+ destination: this.#resourceAddress(destination),
1648
+ retryable: false,
1649
+ });
1650
+ }
1651
+ return this.#writeDestination(statement, source, destination, ctx);
1652
+ }
1653
+ if (destination.lineMarker === null) {
1654
+ return this.#writeDestination(statement, source, destination, ctx);
1655
+ }
1656
+ const moved = await this.#invokeEditBatch(destination, [
1657
+ {
1658
+ marker: destination.lineMarker,
1659
+ body: source.content,
1660
+ tags: Array.isArray(statement.signal) ? statement.signal : [],
1661
+ position: statement.position,
1662
+ },
1663
+ {
1664
+ marker: source.lineMarker,
1665
+ body: "",
1666
+ tags: null,
1667
+ position: statement.position,
1668
+ },
1669
+ ], ctx);
1670
+ const effect = this.#pendingEffect(destination, "update");
1671
+ return this.#finalizeEffects(moved, destination, [effect, effect]);
1672
+ }
1673
+ async #moveOrchestration({ statement, source, destination, ctx, }) {
1674
+ const resolvedSource = await this.#resolveResourceSelection(source, ctx);
1675
+ if (_a.#isDispatchResult(resolvedSource))
1676
+ return resolvedSource;
1677
+ const resolvedDestination = await this.#resolveResourceSelection(destination, ctx);
1678
+ if (_a.#isDispatchResult(resolvedDestination))
1679
+ return resolvedDestination;
1680
+ const selected = await this.#selectSource(resolvedSource, ctx);
1681
+ if (_a.#isDispatchResult(selected))
1682
+ return selected;
1683
+ if (this.#sameChannel(resolvedSource, resolvedDestination)) {
1684
+ const result = await this.#moveWithinChannel(statement, selected, resolvedDestination, ctx);
1685
+ return resolvedDestination.lineMarker === null
1686
+ ? _a.#prependScopeNormalizations(result, selected.scopeNormalizations)
1687
+ : result;
1688
+ }
1689
+ const destinationResult = _a.#prependScopeNormalizations(await this.#writeDestination(statement, selected, resolvedDestination, ctx), selected.scopeNormalizations);
1690
+ if (destinationResult.status >= 400)
1691
+ return destinationResult;
1692
+ const destinationAddress = this.#resourceAddress(resolvedDestination);
1693
+ const destinationEffects = _a.#effectsOf(destinationResult);
1694
+ if (destinationResult.status === 202) {
1695
+ return {
1696
+ ...destinationResult,
1697
+ attrs: {
1698
+ ...destinationResult.attrs,
1699
+ moveSource: this.#deferredMoveSource(resolvedSource, resolvedDestination),
1700
+ },
1701
+ };
1702
+ }
1703
+ const sourceResult = await this.#removeMoveSource(statement, resolvedSource, ctx);
1704
+ if (sourceResult.status === 202) {
1705
+ return {
1706
+ ...sourceResult,
1707
+ ...(destinationResult.scopeNormalizations === undefined
1708
+ ? {}
1709
+ : { scopeNormalizations: destinationResult.scopeNormalizations }),
1710
+ attrs: {
1711
+ ...sourceResult.attrs,
1712
+ moveDestinationWritten: destinationAddress,
1713
+ moveDestinationEffects: destinationEffects,
1714
+ },
1715
+ };
1716
+ }
1717
+ if (sourceResult.status >= 400) {
1718
+ return this.#moveFailureAfterDestination(destinationResult.scopeNormalizations === undefined
1719
+ ? sourceResult
1720
+ : Results.assert({
1721
+ ...sourceResult,
1722
+ scopeNormalizations: destinationResult.scopeNormalizations,
1723
+ }), destinationAddress, destinationEffects);
1724
+ }
1725
+ const base = destinationResult.status === 304
1726
+ ? { ...destinationResult, status: 200 }
1727
+ : destinationResult;
1728
+ return _a.#withCombinedEffects(base, _a.#effectsOf(sourceResult));
1729
+ }
1730
+ async #settleMoveProposal({ statement, result, settlement, ctx, ids, }) {
1731
+ if (statement.op !== "MOVE")
1732
+ return settlement;
1733
+ const attrs = result.attrs;
1734
+ const destinationWritten = attrs?.moveDestinationWritten;
1735
+ if (destinationWritten !== undefined) {
1736
+ const destinationEffects = attrs?.moveDestinationEffects === undefined
1737
+ ? []
1738
+ : assertResourceEffects(attrs.moveDestinationEffects);
1739
+ if (settlement.resolution.decision !== "accept") {
1740
+ const decision = settlement.resolution.decision;
1741
+ return {
1742
+ resolution: settlement.resolution,
1743
+ applied: this.#moveFailureAfterDestination(_a.#failure("move-source-not-applied", decision === "cancel" ? 499 : 409, `The MOVE destination was written, but source removal was ${decision === "cancel" ? "cancelled" : "rejected"}.`, {}, {
1744
+ retryable: false,
1745
+ }), destinationWritten, destinationEffects),
1746
+ };
1747
+ }
1748
+ if (settlement.applied === undefined) {
1749
+ return {
1750
+ resolution: settlement.resolution,
1751
+ applied: this.#moveFailureAfterDestination(_a.#failure("proposal-apply-missing", 500, "The source scheme accepted its MOVE proposal without applying the source mutation.", {}, {
1752
+ stage: "proposal-application",
1753
+ retryable: false,
1754
+ }), destinationWritten, destinationEffects),
1755
+ };
1756
+ }
1757
+ if (settlement.applied.status >= 400) {
1758
+ return {
1759
+ resolution: settlement.resolution,
1760
+ applied: this.#moveFailureAfterDestination(settlement.applied, destinationWritten, destinationEffects),
1761
+ };
1762
+ }
1763
+ return _a.#withSettlementEffects(settlement, [
1764
+ ...destinationEffects,
1765
+ ..._a.#settlementEffects(settlement),
1766
+ ]);
1767
+ }
1768
+ const deferred = attrs?.moveSource;
1769
+ if (deferred === undefined
1770
+ || settlement.resolution.decision !== "accept"
1771
+ || (settlement.applied?.status ?? 200) >= 400) {
1772
+ return settlement;
1773
+ }
1774
+ if (settlement.applied === undefined) {
1775
+ return {
1776
+ resolution: settlement.resolution,
1777
+ applied: _a.#failure("proposal-apply-missing", 500, "The destination scheme accepted its MOVE proposal without applying the destination mutation.", {}, {
1778
+ stage: "proposal-application",
1779
+ retryable: false,
1780
+ }),
1781
+ };
1782
+ }
1783
+ const destinationEffects = _a.#settlementEffects(settlement);
1784
+ const resolvedSource = await this.#resolveResourceSelection({
1785
+ target: deferred.target,
1786
+ lineMarker: deferred.lineMarker,
1787
+ }, ctx);
1788
+ if (_a.#isDispatchResult(resolvedSource)) {
1789
+ return {
1790
+ resolution: settlement.resolution,
1791
+ applied: this.#moveFailureAfterDestination(resolvedSource, deferred.destination, destinationEffects),
1792
+ };
1793
+ }
1794
+ if (resolvedSource.scheme !== deferred.scheme
1795
+ || resolvedSource.pathname !== deferred.pathname
1796
+ || resolvedSource.channel !== deferred.channel) {
1797
+ throw new InvalidOperationResultError("A deferred MOVE source no longer resolves to its recorded identity.");
1798
+ }
1799
+ const removed = await this.#removeMoveSource(statement, resolvedSource, ctx);
1800
+ if (removed.status >= 400) {
1801
+ return {
1802
+ resolution: settlement.resolution,
1803
+ applied: this.#moveFailureAfterDestination(removed, deferred.destination, destinationEffects),
1804
+ };
1805
+ }
1806
+ if (removed.status !== 202) {
1807
+ return _a.#withSettlementEffects(settlement, [
1808
+ ...destinationEffects,
1809
+ ..._a.#effectsOf(removed),
1810
+ ]);
1811
+ }
1812
+ const initialSourceSettlement = await this.#proposals.workerApply(statement, removed, { decision: "accept" }, ids);
1813
+ const sourceSettlement = _a.#settleProposalEffects(removed, initialSourceSettlement);
1814
+ if (sourceSettlement.applied === undefined) {
1815
+ return {
1816
+ resolution: settlement.resolution,
1817
+ applied: this.#moveFailureAfterDestination(_a.#failure("proposal-apply-missing", 500, "The source scheme accepted its MOVE proposal without applying the source mutation.", {}, {
1818
+ stage: "proposal-application",
1819
+ retryable: false,
1820
+ }), deferred.destination, destinationEffects),
1821
+ };
1822
+ }
1823
+ if (sourceSettlement.applied.status >= 400) {
1824
+ return {
1825
+ resolution: settlement.resolution,
1826
+ applied: this.#moveFailureAfterDestination(sourceSettlement.applied, deferred.destination, destinationEffects),
1827
+ };
1828
+ }
1829
+ return _a.#withSettlementEffects(settlement, [
1830
+ ...destinationEffects,
1831
+ ..._a.#settlementEffects(sourceSettlement),
1832
+ ]);
1833
+ }
1834
+ // {§send-premature-terminate} — the unified PENDING SET, judged at the terminal's OWN dispatch
1015
1835
  // (post-batch: the emission's earlier ops already executed, so a same-turn KILL+[200] repairs in
1016
1836
  // ONE turn, and a same-turn WORK+[200] is caught — the spawn is live by the time the SEND lands).
1017
1837
  // pending = open streams ∪ live children ∪ THIS turn's retrievals (READ/FIND/OPEN, results unseen
1018
- // until next packet). One rule, one steer, one repair family: nothing pending may be silently
1019
- // discarded; 499 discards BY STATED INTENT and is never gated.
1838
+ // until next packet). Failed operations are the separate next-packet leg shared by explicit
1839
+ // completion and empty-join completion. Nothing pending may be silently discarded; 499 discards
1840
+ // BY STATED INTENT and is never gated.
1020
1841
  async #pendingSet(workerId, turnId) {
1021
1842
  const pending = [];
1022
1843
  const openSubs = await this.#db.find_open_subscriptions_for_worker.all({ worker_id: workerId });
@@ -1046,7 +1867,7 @@ class Dispatcher {
1046
1867
  this.#db.engine_worker_has_undelivered_stream_term
1047
1868
  .get({ worker_id: workerId }),
1048
1869
  this.#db.engine_worker_has_undelivered_child_term
1049
- .get({ worker_id: workerId, turn_id: turnId }),
1870
+ .get({ worker_id: workerId }),
1050
1871
  ]);
1051
1872
  return {
1052
1873
  retrievals: retrievals.length > 0,
@@ -1054,8 +1875,24 @@ class Dispatcher {
1054
1875
  childTerminations: childTermination !== undefined,
1055
1876
  };
1056
1877
  }
1878
+ // A failed operation is also an unobserved result: it does not enter the
1879
+ // model's Log until the next packet. Both explicit completion and an
1880
+ // already-drained join must cross that observation boundary before they
1881
+ // can honestly finish the loop.
1882
+ async #unobservedFailureCount(turnId) {
1883
+ const failedRows = await this.#db.engine_turn_failures.all({ turn_id: turnId });
1884
+ return failedRows.length;
1885
+ }
1886
+ static #unobservedFailures(failCount) {
1887
+ return _a.#failure("unobserved-failures", 409, `Completion was refused because ${failCount} failed operation(s) from this turn have not been observed.`, {}, {
1888
+ failures: failCount,
1889
+ stage: "completion",
1890
+ recovery: "Review the failed log items before concluding.",
1891
+ retryable: false,
1892
+ });
1893
+ }
1057
1894
  // J — a live obligation to WAIT on: a spawned child or an open stream (NOT retrievals, which land
1058
- // next turn regardless). The wait-side twin of #pendingSet's stream+child legs (§wait-obligation-matrix).
1895
+ // next turn regardless). The wait-side twin of #pendingSet's stream+child legs ({§wait-obligation-matrix}).
1059
1896
  async #hasLiveWork(workerId) {
1060
1897
  const openSubs = await this.#db.find_open_subscriptions_for_worker.all({ worker_id: workerId });
1061
1898
  if (openSubs.length > 0)
@@ -1071,40 +1908,42 @@ class Dispatcher {
1071
1908
  throw new Error("unreachable");
1072
1909
  const { workerId, loopId, turnId } = ctx;
1073
1910
  const status = statement.signal;
1074
- if (status === null)
1075
- return { status: 400 };
1911
+ if (status === null) {
1912
+ return _a.#failure("send-status-required", 400, "SEND requires a numeric status.", {}, { retryable: false });
1913
+ }
1076
1914
  const raw = statement.body === null ? "" : typeof statement.body === "string" ? statement.body : statement.body.raw;
1077
- // [102]<T> waiting is a mode of CONTINUING (grammar 0.75.0, the terminal redesign): park
1078
- // up to T seconds, woken early by any arrival (stream/child conclusion, irc, inject), woken
1079
- // at T regardless — a park ALWAYS has a next turn, so nothing can be orphaned. <-1> parks
1080
- // indefinitely (the butler/worker pattern owner-ruled ungated; the instrumentation renders
1081
- // it legibly). Internally the parked state remains loops.status=202: the model-facing SIGNAL
1082
- // retired, the engine's park state did not.
1083
- // §join-blocking-collect (#354) — a READ(worker://running-child) this turn armed a join. A BARE
1084
- // continue becomes an indefinite PARK: the blocking collect, on the same park machinery <-1>
1085
- // uses. The armed READ said "I want this result"; parking IS the collect (the model never had
1086
- // to know the park syntax — the engine holds the join). Any SEND clears the per-turn arm; a
1087
- // terminal with a live child stays the existing premature-terminate steer (#354 decision #1).
1915
+ // The park rides SEND[202] only ({§park-202-only}). A scoped SEND[102] is neither
1916
+ // a wait nor a meaningful continuation, so reject it instead of preserving the
1917
+ // retired dual spelling.
1918
+ if (status === 102 && statement.lineMarker !== null) {
1919
+ return _a.#failure("send-scope-invalid", 400, "SEND[102] does not accept a scope.", {}, {
1920
+ requestedStatus: 102,
1921
+ scope: statement.lineMarker,
1922
+ recovery: "Use SEND[202] with a scope to wait, or remove the scope to continue.",
1923
+ retryable: false,
1924
+ });
1925
+ }
1926
+ // A bare continue after an armed running-worker READ becomes an
1927
+ // indefinite park. {§join-blocking-collect}
1088
1928
  const joinArmed = this.#joinTargets.delete(loopId);
1089
1929
  if (status === 102 && statement.lineMarker === null && joinArmed) {
1090
1930
  if (!await this.#lifecycle.park(loopId, raw.length > 0 ? raw : "parked — awaiting a worker's result (blocking collect)")) {
1091
- return { status: await this.#lifecycle.status(loopId) };
1931
+ return _a.#statusResult(await this.#lifecycle.status(loopId), "loop-already-terminal", "The loop was already terminal when SEND attempted to park it.");
1092
1932
  }
1093
1933
  this.#parkDeadlines.set(loopId, -1); // indefinite: the bounded child's terminal is the wake edge
1094
1934
  return { status: 102, attrs: { parked: -1, join: true } };
1095
1935
  }
1096
- // §wait-obligation-matrix — SEND[202] and the legacy SEND[102]<T> are one
1097
- // obligation-checked join. A live
1936
+ // {§wait-obligation-matrix} — SEND[202] is the obligation-checked join. A live
1098
1937
  // obligation (a spawned child or open stream, J) BLOCKS the loop until it concludes and
1099
- // reawakens it (§worker-lifecycle-child-wake); a wait on nothing (∅) is already satisfied and
1938
+ // reawakens it ({§worker-lifecycle-child-wake}); a wait on nothing (∅) is already satisfied and
1100
1939
  // resolves like 200, so <-1>+∅ self-resolves rather than hang the agent; a pending own
1101
1940
  // retrieval (R) just lands next turn, so the wait continues.
1102
- if (status === 202 || (status === 102 && statement.lineMarker !== null)) {
1941
+ if (status === 202) {
1103
1942
  const marks = statement.lineMarker?.marks[0];
1104
1943
  const seconds = typeof marks === "number" ? marks : -1; // bare 202 / absent T = indefinite, bounded by the join
1105
1944
  if (await this.#hasLiveWork(workerId)) {
1106
1945
  if (!await this.#lifecycle.park(loopId, raw.length > 0 ? raw : "waiting on live work")) {
1107
- return { status: await this.#lifecycle.status(loopId) };
1946
+ return _a.#statusResult(await this.#lifecycle.status(loopId), "loop-already-terminal", "The loop was already terminal when SEND attempted to wait.");
1108
1947
  }
1109
1948
  this.#parkDeadlines.set(loopId, seconds);
1110
1949
  return { status: 202, attrs: { waiting: seconds } };
@@ -1117,15 +1956,21 @@ class Dispatcher {
1117
1956
  if (observations.retrievals || observations.streamTerminations || observations.childTerminations) {
1118
1957
  return { status: 102 };
1119
1958
  }
1959
+ const failCount = await this.#unobservedFailureCount(turnId);
1960
+ if (failCount > 0)
1961
+ return _a.#unobservedFailures(failCount);
1962
+ const branchDenial = await this.#branchCompletionGate?.(workerId) ?? null;
1963
+ if (branchDenial !== null)
1964
+ return branchDenial;
1120
1965
  // The joined set is already drained. Awaiting an empty task group completes
1121
1966
  // immediately; it never parks and needs no corrective model turn.
1122
- const finished = await this.#lifecycle.finish(loopId, 200, raw === "" ? null : raw);
1967
+ const finished = await this.#lifecycle.finish(loopId, { status: 200 }, { message: raw === "" ? null : raw });
1123
1968
  return {
1124
- status: finished ? 200 : await this.#lifecycle.status(loopId),
1969
+ status: finished !== null ? 200 : await this.#lifecycle.status(loopId),
1125
1970
  attrs: { joined: true, pending: 0 },
1126
1971
  };
1127
1972
  }
1128
- // §send-300-choices — ask the operator and park (the answer returns via loop.inject).
1973
+ // {§send-300-choices} — ask the operator and park (the answer returns via loop.inject).
1129
1974
  // The three-state cascade (owner design): PLURNK_QUESTIONS unset = ALLOWED (not enabled);
1130
1975
  // =0 = DENIED servicewide (a ceiling the client cannot override); ENABLED requires the
1131
1976
  // client to affirmatively pass it per workspace (settings.questions — the interactive client
@@ -1134,14 +1979,13 @@ class Dispatcher {
1134
1979
  // Disabled → refused with a self-decide steer, never a park into the void.
1135
1980
  if (status === 300) {
1136
1981
  if (!(await WorkspaceSettings.questionsEnabled(this.#db, ctx.workspaceId))) {
1137
- return { status: 409, error: "Operator asks ([300]) are not enabled in this environment — no one is watching to answer. Decide yourself and proceed: SEND[102] to continue, or [200] to conclude." };
1982
+ return _a.#failure("operator-questions-disabled", 409, "Operator questions are disabled in this workspace.", {}, {
1983
+ recovery: "Decide from the available evidence and continue or conclude.",
1984
+ retryable: false,
1985
+ });
1138
1986
  }
1139
- // Owner ruling (#346): the question rides the SAME proposal system as file edits and
1140
- // MCP auths — stop the world. Returning 202 here routes through the proposal seam
1141
- // (#isProposal admits signal-300 SENDs): loop/proposal carries {question, choices},
1142
- // loop.resolve's accept body IS the answer, and applyResolution writes it into the
1143
- // model-facing rx — the answer arrives as the result of the ask itself. Timeout is
1144
- // the standard §proposal-timeout-cancels. Zero options = an open question.
1987
+ // {§send-300-choices} return a proposal whose accepted body becomes
1988
+ // the answer; zero choices denotes an open question.
1145
1989
  const parts = raw.split(";").map((x) => x.trim()).filter((x) => x.length > 0);
1146
1990
  const [question = "", ...choices] = parts;
1147
1991
  return { status: 202, attrs: choices.length > 0 ? { question, choices } : { question } };
@@ -1150,77 +1994,90 @@ class Dispatcher {
1150
1994
  // attempt faithfully (status_rx=409, never erased); the loop stays a continue; the strike
1151
1995
  // couples in runTurn. [499] abandons and cancels the descendant scope.
1152
1996
  if (status === 200) {
1153
- // §send-200-failed-ops (#363, owner ruling: never 200 over a failed op) — the failure
1154
- // twin of the pending set: this turn's failed op results (and this emission's parse
1155
- // errors, threaded they mint as rows only after dispatch) are UNSEEN until the next
1156
- // packet, so concluding over them is concluding blind. Refused 409; next turn, the
1157
- // errors in-log and weighed, [200] stands. [499] below is never gated — declaring
1158
- // failure IS weighing it.
1159
- const failedRows = await this.#db.engine_turn_failures.all({ turn_id: turnId });
1160
- const failCount = failedRows.length + (ctx.turnParseErrors ?? 0);
1161
- if (failCount > 0) {
1162
- return { status: 409, error: `Termination attempted despite ${failCount} failed operation(s) this turn. The errors land in your log next turn — weigh them, then conclude (or SEND[499] to abandon).` };
1163
- }
1997
+ // {§send-premature-terminate} same-turn failures are unobserved
1998
+ // pending results and therefore refuse completion.
1999
+ const failCount = await this.#unobservedFailureCount(turnId);
2000
+ if (failCount > 0)
2001
+ return _a.#unobservedFailures(failCount);
1164
2002
  const pending = await this.#pendingSet(workerId, turnId);
1165
2003
  if (pending.length > 0) {
1166
- // Kind-specific steer (owner wording, xpath/topo forensics): retrievals-only is
1167
- // gemma's read-and-conclude idiom no lever to pull, the results simply arrive;
1168
- // KILL/park advice only muddies it. Streams/children keep the remedy steer.
2004
+ // A retrieval-only refusal needs no KILL/park remedy menu: the results simply
2005
+ // arrive in the next packet. Streams and children retain their remedy steer.
1169
2006
  const retrievalsOnly = pending.every((k) => k.startsWith("this turn's retrieval results"));
1170
2007
  if (retrievalsOnly) {
1171
- // attrs.retrievalOnly the strike decoupler (owner ruling): atomic-turn-
1172
- // pretrained models pair fetch-and-answer by habit; the refusal teaches,
1173
- // the strike executed. Engine reads this to skip the strike.
1174
- // #384 (owner wording, run48 requiem) three lessons in one steer: it narrates
1175
- // "last turn" as HISTORY (the model reads log rows in the third person and never
1176
- // took "you"-steers as self-addressed), states the mechanism as law (retrievals
1177
- // FORCE an additional turn — not blame, physics), and prescribes the concluding
1178
- // emission's legal SHAPE (PLAN + SEND[200] only — no room for the justify-READ
1179
- // that re-armed this gate four times while she held the correct answer).
1180
- return { status: 409, error: "Last turn both performed retrieval operations and attempted to terminate. Retrieval operations force an additional turn to receive results for review and reaction. To conclude, only use PLAN and SEND[200] operations.", attrs: { retrievalOnly: true } };
2008
+ return _a.#failure("retrieval-results-unobserved", 409, "Last turn both performed retrieval operations and attempted to terminate. Retrieval operations force an additional turn so their results can be reviewed.", {}, {
2009
+ pending: [...pending],
2010
+ stage: "completion",
2011
+ recovery: "Review the results, then use only PLAN and SEND[200] to conclude.",
2012
+ retryable: false,
2013
+ });
1181
2014
  }
1182
- return { status: 409, error: `Attempted [200] termination in a turn that performs retrieval or has surviving work: ${pending.join("; ")}. KILL what you no longer need; SEND[102] (or [102]<seconds>) to receive the rest; then conclude.` };
2015
+ return _a.#failure("work-remains", 409, `Completion was refused while work remains: ${pending.join("; ")}.`, {}, {
2016
+ pending: [...pending],
2017
+ stage: "completion",
2018
+ recovery: "Resolve the listed pending work before concluding.",
2019
+ retryable: false,
2020
+ });
1183
2021
  }
1184
- const finished = await this.#lifecycle.finish(loopId, 200, raw === "" ? null : raw);
1185
- return { status: finished ? 200 : await this.#lifecycle.status(loopId) };
2022
+ const branchDenial = await this.#branchCompletionGate?.(workerId) ?? null;
2023
+ if (branchDenial !== null)
2024
+ return branchDenial;
2025
+ const finished = await this.#lifecycle.finish(loopId, { status: 200 }, { message: raw === "" ? null : raw });
2026
+ return _a.#statusResult(finished !== null ? 200 : await this.#lifecycle.status(loopId), "loop-already-terminal", "The loop was already terminal when SEND attempted to conclude it.");
1186
2027
  }
1187
2028
  if (status === 499) {
1188
- const finished = await this.#lifecycle.finish(loopId, 499, raw === "" ? null : raw);
1189
- if (!finished)
1190
- return { status: await this.#lifecycle.status(loopId) };
2029
+ const branchDenial = await this.#branchCompletionGate?.(workerId) ?? null;
2030
+ if (branchDenial !== null)
2031
+ return branchDenial;
2032
+ const failure = _a.#failure("scope-abandoned", 499, raw === "" ? "The worker abandoned its scope." : raw);
2033
+ const seqs = await this.#db.engine_loop_turn_seqs.get({
2034
+ loop_id: loopId,
2035
+ turn_id: turnId,
2036
+ });
2037
+ if (seqs === undefined) {
2038
+ throw new Error(`SEND[499]: no coordinate for loop=${loopId} turn=${turnId}`);
2039
+ }
2040
+ Results.attachInstance(failure, `log:///${seqs.loop_seq}/${seqs.turn_seq}/${ctx.sequence}/SEND`);
2041
+ const finished = await this.#lifecycle.finish(loopId, failure);
2042
+ if (finished === null)
2043
+ return _a.#statusResult(await this.#lifecycle.status(loopId), "loop-already-terminal", "The loop was already terminal when SEND attempted to abandon it.");
1191
2044
  await this.#cancelDescendants?.(workerId, raw === "" ? "parent abandoned its scope" : raw);
1192
- return { status: 499 };
2045
+ return failure;
1193
2046
  }
1194
2047
  // Every other signal — 102 bare, 202 (retired as a terminal; now ordinary mid-comms), 1xx —
1195
2048
  // is a plain broadcast row: no loop transition.
1196
- return { status };
2049
+ return _a.#statusResult(status, "send-broadcast-failed", raw === "" ? `SEND broadcast reported status ${status}.` : raw);
1197
2050
  }
1198
2051
  async #run(schemeName, statement, ctx) {
1199
- if (schemeName === null)
1200
- return { status: 400 };
2052
+ if (schemeName === null) {
2053
+ return _a.#failure("target-scheme-required", 400, `${statement.op} requires a target scheme.`, {}, { operation: statement.op, retryable: false });
2054
+ }
1201
2055
  if (statement.op === "SEND" && statement.signal === 499 && statement.target?.kind === "url") {
2056
+ const addressedScheme = statement.target.scheme;
1202
2057
  const entry = await this.#db.crud_find_workspace_entry.get({
1203
2058
  workspace_id: ctx.workspaceId,
1204
2059
  owner_id: await Owner.commonsId(this.#db, ctx.workspaceId),
1205
- scheme: schemeName,
1206
- pathname: pathnameFromPath(statement.target),
2060
+ scheme: addressedScheme,
2061
+ pathname: entryPathnameOf(statement.target),
1207
2062
  });
1208
2063
  if (entry !== undefined) {
1209
2064
  const subscription = await ChannelWrite.findActiveSubscription(this.#db, {
1210
2065
  workerId: ctx.workerId,
1211
2066
  entryId: entry.id,
1212
2067
  });
1213
- if (subscription !== null && subscription.scheme === schemeName) {
2068
+ if (subscription !== null && subscription.scheme === addressedScheme) {
1214
2069
  const cancelled = await this.#liveSubscriptions.cancel(subscription.id);
1215
- return cancelled
1216
- ? { status: 200 }
1217
- : { status: 500, error: `subscription ${subscription.id} is durable but has no live cancellation handle` };
2070
+ if (!cancelled) {
2071
+ throw new InvalidOperationResultError(`Subscription ${subscription.id} is durable but has no live cancellation handle.`);
2072
+ }
2073
+ return { status: 200 };
1218
2074
  }
1219
2075
  }
1220
2076
  }
1221
2077
  const handler = this.#schemes.get(schemeName);
1222
- if (handler === undefined)
1223
- return { status: 501 };
2078
+ if (handler === undefined) {
2079
+ return _a.#failure("scheme-not-found", 501, `Scheme '${schemeName}' is not registered.`, {}, { scheme: schemeName, retryable: false });
2080
+ }
1224
2081
  const methodName = statement.op.toLowerCase();
1225
2082
  const method = handler[methodName];
1226
2083
  const addressedScheme = statement.target?.kind === "url" ? statement.target.scheme : null;
@@ -1229,26 +2086,45 @@ class Dispatcher {
1229
2086
  throw new Error(`scheme '${schemeName}' has no manifest`);
1230
2087
  const schemeCtx = new SchemeCtxImpl(ctx, addressedScheme ?? schemeName, manifest, this.#liveSubscriptions);
1231
2088
  if (typeof method === "function")
1232
- return method.call(handler, statement, schemeCtx);
1233
- if (statement.op !== "FIND" || manifest.category !== "data")
1234
- return { status: 501 };
2089
+ return Results.assert(await method.call(handler, statement, schemeCtx));
2090
+ if (statement.op !== "FIND" || manifest.category !== "data") {
2091
+ return _a.#failure("operation-not-implemented", 501, `Scheme '${schemeName}' does not implement ${statement.op}.`, {}, {
2092
+ scheme: schemeName,
2093
+ operation: statement.op,
2094
+ retryable: false,
2095
+ });
2096
+ }
1235
2097
  const prepareFind = handler.prepareFind;
1236
2098
  if (typeof prepareFind === "function") {
1237
2099
  const prepared = await prepareFind.call(handler, statement, schemeCtx);
1238
2100
  if (prepared.status >= 300)
1239
- return prepared;
2101
+ return Results.assert(prepared);
1240
2102
  }
1241
- return schemeCtx.entries.operations.find(statement);
2103
+ return Results.assert(await schemeCtx.entries.operations.find(statement));
1242
2104
  }
1243
- // A status-202 result is a reviewable PROPOSAL (a side-effecting op — EDIT/EXEC/
1244
- // directed write — paused for client resolution) UNLESS it is a broadcast SEND.
1245
- // A broadcast SEND[202] is the model PARKING the loop (a terminal disposition,
1246
- // plurnk.md), never a side-effect #255: gating the propose/await path on the
1247
- // bare 202 surfaced model speech as a loop/proposal and froze clients. The 202
1248
- // is overloaded (proposal-pause vs parked-terminal); the op disambiguates it.
2105
+ async #runLogCuration(statement, ctx) {
2106
+ const addressedScheme = schemeNameOf(statement.target);
2107
+ if (addressedScheme !== null && addressedScheme !== "log") {
2108
+ return _a.#failure("operation-not-implemented", 501, `Scheme '${addressedScheme}' does not implement ${statement.op}.`, {}, {
2109
+ scheme: addressedScheme,
2110
+ operation: statement.op,
2111
+ retryable: false,
2112
+ });
2113
+ }
2114
+ const handler = this.#schemes.get("log");
2115
+ const manifest = this.#schemes.manifestFor("log");
2116
+ if (handler === undefined || manifest === undefined) {
2117
+ throw new Error("the core log curation owner is not registered");
2118
+ }
2119
+ const schemeCtx = new SchemeCtxImpl(ctx, "log", manifest, this.#liveSubscriptions);
2120
+ return statement.op === "OPEN"
2121
+ ? Results.assert(await handler.open(statement, schemeCtx))
2122
+ : Results.assert(await handler.fold(statement, schemeCtx));
2123
+ }
2124
+ // {§proposal}/{§send} — status 202 is a proposal except for broadcast
2125
+ // SEND[202], which parks the loop. The operation disambiguates the status.
1249
2126
  static #isProposal(statement, result) {
1250
- // A broadcast SEND park is model speech, not a proposal (#255) — EXCEPT a [300] question,
1251
- // which IS a proposal by owner ruling (#346: the same stop-the-world system as file edits).
2127
+ // {§send-300-choices} SEND[300] is the proposal exception.
1252
2128
  if (result.status !== 202)
1253
2129
  return false;
1254
2130
  if (statement.op === "SEND" && statement.signal === 300)
@@ -1256,21 +2132,39 @@ class Dispatcher {
1256
2132
  return !(statement.op === "SEND" && statement.target === null);
1257
2133
  }
1258
2134
  async #writeLog({ statement, result, workspaceId, workerId, loopId, turnId, sequence, origin, }) {
1259
- const target = this.#extractTarget(statement.target);
1260
- await this.#canonColumns(target, workspaceId); // {§fs-answer-in-canon} — columns speak canon; tx below stays verbatim
1261
- const lineMarkerJson = "lineMarker" in statement && statement.lineMarker !== null
1262
- ? JSON.stringify(statement.lineMarker)
2135
+ const durableStatement = DurableStatement.project(statement);
2136
+ const target = this.#extractTarget(durableStatement.target);
2137
+ await this.#canonColumns(target, workspaceId); // {§fs-answer-in-canon}
2138
+ const lineMarkerJson = "lineMarker" in durableStatement && durableStatement.lineMarker !== null
2139
+ ? JSON.stringify(durableStatement.lineMarker)
1263
2140
  : null;
1264
2141
  // A proposal (status 202 from a side-effecting op) is written to the log in
1265
2142
  // state='proposed' until the proposal lifecycle resolves it; attrs holds the
1266
2143
  // scheme-supplied payload (file diff, exec command, etc.) the client renders
1267
2144
  // for review and the scheme consumes on accept. A broadcast SEND[202] is a
1268
- // parked-terminal, NOT a proposal (#isProposal / #255) → state='resolved'.
2145
+ // parked-terminal, not a proposal (#isProposal) → state='resolved'.
1269
2146
  const isProposed = _a.#isProposal(statement, result);
1270
2147
  let attrsObj = (result.attrs !== undefined && result.attrs !== null)
1271
2148
  ? { ...result.attrs }
1272
2149
  : {};
1273
- // EXEC produces a stream entry addressed by RUNTIME TAG as authority (§exec): it lives
2150
+ const seqs = statement.op === "EXEC" || result.problem !== undefined
2151
+ ? await this.#db.engine_loop_turn_seqs.get({
2152
+ loop_id: loopId,
2153
+ turn_id: turnId,
2154
+ })
2155
+ : undefined;
2156
+ if ((statement.op === "EXEC" || result.problem !== undefined) && seqs === undefined) {
2157
+ throw new Error(`Dispatcher.#writeLog: loop_turn_seqs returned no row for loop=${loopId} turn=${turnId}`);
2158
+ }
2159
+ if (statement.op === "READ")
2160
+ Results.assertReadResult(result);
2161
+ if (result.problem !== undefined && seqs !== undefined) {
2162
+ Results.attachInstance(result, `log:///${seqs.loop_seq}/${seqs.turn_seq}/${sequence}/${statement.op}`);
2163
+ }
2164
+ else {
2165
+ Results.assert(result);
2166
+ }
2167
+ // EXEC produces a stream entry addressed by RUNTIME TAG as authority ({§exec}): it lives
1274
2168
  // at <runtime>:///<loop_seq>/<turn_seq>/<sequence> (e.g. sh:///1/1/2). That address is a
1275
2169
  // SEPARATE `stream` link in attrs — NOT an overload of `target`, which stays faithful to
1276
2170
  // the EXEC's own slot (the cwd, or the path to the executable). The log:/// coordinate
@@ -1278,11 +2172,8 @@ class Dispatcher {
1278
2172
  // Runtime comes from statement.signal (EXEC's runtime slot), resolvable for failed execs
1279
2173
  // too; empty/absent = the default shell.
1280
2174
  if (statement.op === "EXEC") {
1281
- const seqs = await this.#db.engine_loop_turn_seqs.get({
1282
- loop_id: loopId, turn_id: turnId,
1283
- });
1284
2175
  if (seqs === undefined)
1285
- throw new Error(`Dispatcher.#writeLog: loop_turn_seqs returned no row for loop=${loopId} turn=${turnId}`);
2176
+ throw new Error("Dispatcher.#writeLog: EXEC coordinate was not resolved");
1286
2177
  const runtime = (typeof statement.signal === "string" && statement.signal.length > 0) ? statement.signal : "sh";
1287
2178
  const coordPathname = `/${seqs.loop_seq}/${seqs.turn_seq}/${sequence}`;
1288
2179
  attrsObj.pathname = coordPathname;
@@ -1297,7 +2188,7 @@ class Dispatcher {
1297
2188
  }
1298
2189
  }
1299
2190
  const attrs = JSON.stringify(attrsObj);
1300
- const txJson = JSON.stringify(statement);
2191
+ const txJson = JSON.stringify(durableStatement);
1301
2192
  const rxJson = JSON.stringify(result);
1302
2193
  const row = await this.#db.engine_insert_log_entry.get({
1303
2194
  worker_id: workerId,
@@ -1305,17 +2196,17 @@ class Dispatcher {
1305
2196
  turn_id: turnId,
1306
2197
  sequence: sequence,
1307
2198
  origin,
1308
- source: null, // dispatch entries are self-authored; §env-delta deltas set this
1309
- op: statement.op,
1310
- suffix: statement.suffix,
1311
- signal: this.#signalToJson(statement.signal),
2199
+ source: null, // dispatch entries are self-authored; {§env-delta} deltas set this
2200
+ op: durableStatement.op,
2201
+ suffix: durableStatement.suffix,
2202
+ signal: this.#signalToJson(durableStatement.signal),
1312
2203
  scheme: target.scheme,
1313
2204
  username: target.username,
1314
2205
  password: target.password,
1315
2206
  hostname: target.hostname,
1316
2207
  port: target.port,
1317
2208
  pathname: target.pathname,
1318
- params: target.params,
2209
+ query: target.query,
1319
2210
  fragment: target.fragment,
1320
2211
  lineMarker: lineMarkerJson,
1321
2212
  tx: txJson,
@@ -1332,31 +2223,29 @@ class Dispatcher {
1332
2223
  throw new Error("Dispatcher.#writeLog: INSERT ... RETURNING produced no row");
1333
2224
  return row.id;
1334
2225
  }
1335
- // Normalize a parsed path for storage. The `file` scheme is a routing
1336
- // internal never stored, never rendered to the model. Both bare paths
1337
- // and `file:///...` inputs collapse to scheme=null at this boundary, so
1338
- // entries.scheme / log_entries.scheme never carry the string "file".
2226
+ // Normalize a parsed target for log storage. Bare paths and `file:///...`
2227
+ // inputs collapse to scheme=null in log target metadata because both render
2228
+ // as bare paths. Addressable file entries separately persist under the
2229
+ // reserved `file` identity scheme ({§entry-identity-no-null}).
1339
2230
  #extractTarget(path) {
1340
2231
  if (path === null)
1341
- return { scheme: null, username: null, password: null, hostname: null, port: null, pathname: null, params: null, fragment: null };
1342
- // `local` (bare path) and `regex` (grammar 0.46 `#pattern#flags` target) carry no URL parts — store the raw text as the pathname for the log record, scheme=null.
1343
- if (path.kind === "regex")
1344
- return { scheme: null, username: null, password: null, hostname: null, port: null, pathname: path.raw, params: null, fragment: null }; // regex source — no decode
2232
+ return { scheme: null, username: null, password: null, hostname: null, port: null, pathname: null, query: null, fragment: null };
2233
+ // `local` (bare path) carries no URL parts — store the raw text as the pathname for the log record, scheme=null.
1345
2234
  if (path.kind === "local")
1346
- return { scheme: null, username: null, password: null, hostname: null, port: null, pathname: decodePathParens(path.raw), params: null, fragment: null }; // #239 item 4
2235
+ return { scheme: null, username: null, password: null, hostname: null, port: null, pathname: PathSyntax.decodeParens(path.raw), query: null, fragment: null }; // {§path-parentheses}
1347
2236
  const scheme = path.scheme === "file" ? null : path.scheme;
1348
- // Every registered (plurnk-namespace) scheme uses its authority as a namespace segment — fold
1349
- // it into the canonical pathname so known://x known:///x ≡ /x and the log keys identically to
1350
- // the entry (/prompt/<run>/<loop>/<N>, /docs/x.md). A foreign web host (http://, unregistered) is NOT a
1351
- // namespace: keep it in hostname. worker:// is the one registered EXCEPTION — its authority IS the
1352
- // run selector (§worker-scheme), and worker://self must stay distinct from worker://name, so Run.ts
1353
- // folds the owner into the storage path itself, never here.
1354
- const foldNs = scheme !== null && scheme !== "worker" && this.#schemes.has(scheme);
2237
+ // {§scheme-address-namespace-fold} — a registered non-network scheme folds its
2238
+ // namespace authority into the canonical pathname. Network authorities remain hosts;
2239
+ // worker:// is the registered exception because its authority selects the owner.
2240
+ const foldNs = scheme !== null
2241
+ && scheme !== "worker"
2242
+ && !NetworkAddress.supports(scheme)
2243
+ && this.#schemes.has(scheme);
1355
2244
  return {
1356
2245
  scheme, username: path.username, password: path.password,
1357
2246
  hostname: foldNs ? null : path.hostname, port: path.port,
1358
- pathname: decodePathParens(foldNs ? foldAuthorityIntoPath(path.hostname, path.pathname) : path.pathname), // #239 item 4
1359
- params: JSON.stringify(path.params), fragment: path.fragment,
2247
+ pathname: PathSyntax.decodeParens(foldNs ? foldAuthorityIntoPath(path.hostname, path.pathname) : path.pathname), // {§path-parentheses}
2248
+ query: path.query, fragment: path.fragment,
1360
2249
  };
1361
2250
  }
1362
2251
  #signalToJson(signal) {