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