@plurnk/plurnk-service 1.7.0 → 1.9.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 (381) hide show
  1. package/.env.defaults +49 -42
  2. package/INSTALL.md +48 -11
  3. package/README.md +12 -2
  4. package/SPEC.md +1166 -475
  5. package/digest-sql/channel-dispositions/dispositions.sql +15 -0
  6. package/digest-sql/channel-state/state.sql +6 -0
  7. package/digest-sql/curation/curation.sql +5 -0
  8. package/digest-sql/derivations/derivations.sql +4 -0
  9. package/digest-sql/embeddings/embeddings.sql +4 -0
  10. package/digest-sql/entry-dispositions/dispositions.sql +15 -0
  11. package/digest-sql/entry-state/state.sql +6 -0
  12. package/digest-sql/token-counts/token-counts.sql +2 -0
  13. package/dist/Paths.d.ts +0 -4
  14. package/dist/Paths.d.ts.map +1 -1
  15. package/dist/Paths.js +1 -21
  16. package/dist/Paths.js.map +1 -1
  17. package/dist/build-info.json +1 -1
  18. package/dist/content/read-projector.d.ts.map +1 -1
  19. package/dist/content/read-projector.js +4 -3
  20. package/dist/content/read-projector.js.map +1 -1
  21. package/dist/core/BudgetReadout.d.ts.map +1 -1
  22. package/dist/core/BudgetReadout.js +12 -22
  23. package/dist/core/BudgetReadout.js.map +1 -1
  24. package/dist/core/ChannelWrite.d.ts +8 -3
  25. package/dist/core/ChannelWrite.d.ts.map +1 -1
  26. package/dist/core/ChannelWrite.js +40 -8
  27. package/dist/core/ChannelWrite.js.map +1 -1
  28. package/dist/core/ChannelWrite.sql +19 -5
  29. package/dist/core/CoreSchemeServices.d.ts +7 -3
  30. package/dist/core/CoreSchemeServices.d.ts.map +1 -1
  31. package/dist/core/CoreSchemeServices.js +8 -1
  32. package/dist/core/CoreSchemeServices.js.map +1 -1
  33. package/dist/core/Dispatcher.d.ts +15 -6
  34. package/dist/core/Dispatcher.d.ts.map +1 -1
  35. package/dist/core/Dispatcher.js +352 -306
  36. package/dist/core/Dispatcher.js.map +1 -1
  37. package/dist/core/Engine.d.ts +22 -10
  38. package/dist/core/Engine.d.ts.map +1 -1
  39. package/dist/core/Engine.js +86 -46
  40. package/dist/core/Engine.js.map +1 -1
  41. package/dist/core/Engine.sql +202 -177
  42. package/dist/core/EntryAddressBinding.d.ts +32 -0
  43. package/dist/core/EntryAddressBinding.d.ts.map +1 -0
  44. package/dist/core/EntryAddressBinding.js +93 -0
  45. package/dist/core/EntryAddressBinding.js.map +1 -0
  46. package/dist/core/ExecutorRegistry.d.ts +12 -6
  47. package/dist/core/ExecutorRegistry.d.ts.map +1 -1
  48. package/dist/core/ExecutorRegistry.js +34 -31
  49. package/dist/core/ExecutorRegistry.js.map +1 -1
  50. package/dist/core/HostPaths.d.ts +29 -0
  51. package/dist/core/HostPaths.d.ts.map +1 -0
  52. package/dist/core/HostPaths.js +74 -0
  53. package/dist/core/HostPaths.js.map +1 -0
  54. package/dist/core/LegacyHome.d.ts +10 -0
  55. package/dist/core/LegacyHome.d.ts.map +1 -0
  56. package/dist/core/LegacyHome.js +170 -0
  57. package/dist/core/LegacyHome.js.map +1 -0
  58. package/dist/core/LogBody.d.ts +1 -1
  59. package/dist/core/LogBody.d.ts.map +1 -1
  60. package/dist/core/LogBody.js +21 -3
  61. package/dist/core/LogBody.js.map +1 -1
  62. package/dist/core/LogEntryProjection.js +1 -1
  63. package/dist/core/LogEntryProjection.js.map +1 -1
  64. package/dist/core/LogVisibility.d.ts +28 -0
  65. package/dist/core/LogVisibility.d.ts.map +1 -0
  66. package/dist/core/LogVisibility.js +215 -0
  67. package/dist/core/LogVisibility.js.map +1 -0
  68. package/dist/core/LoopLifecycle.d.ts +1 -0
  69. package/dist/core/LoopLifecycle.d.ts.map +1 -1
  70. package/dist/core/LoopLifecycle.js +8 -0
  71. package/dist/core/LoopLifecycle.js.map +1 -1
  72. package/dist/core/LoopLifecycle.sql +10 -0
  73. package/dist/core/OperatorConfig.d.ts +6 -0
  74. package/dist/core/OperatorConfig.d.ts.map +1 -0
  75. package/dist/core/OperatorConfig.js +95 -0
  76. package/dist/core/OperatorConfig.js.map +1 -0
  77. package/dist/core/OverflowTurn.d.ts +11 -0
  78. package/dist/core/OverflowTurn.d.ts.map +1 -0
  79. package/dist/core/OverflowTurn.js +81 -0
  80. package/dist/core/OverflowTurn.js.map +1 -0
  81. package/dist/core/Owner.js +3 -3
  82. package/dist/core/Owner.js.map +1 -1
  83. package/dist/core/PacketBuilder.d.ts +7 -15
  84. package/dist/core/PacketBuilder.d.ts.map +1 -1
  85. package/dist/core/PacketBuilder.js +64 -127
  86. package/dist/core/PacketBuilder.js.map +1 -1
  87. package/dist/core/ProblemLog.js +1 -1
  88. package/dist/core/ProblemLog.js.map +1 -1
  89. package/dist/core/ProposalLifecycle.d.ts +4 -1
  90. package/dist/core/ProposalLifecycle.d.ts.map +1 -1
  91. package/dist/core/ProposalLifecycle.js +86 -47
  92. package/dist/core/ProposalLifecycle.js.map +1 -1
  93. package/dist/core/ProviderInstantiate.d.ts +9 -5
  94. package/dist/core/ProviderInstantiate.d.ts.map +1 -1
  95. package/dist/core/ProviderInstantiate.js +99 -104
  96. package/dist/core/ProviderInstantiate.js.map +1 -1
  97. package/dist/core/ReasoningEvent.d.ts +13 -0
  98. package/dist/core/ReasoningEvent.d.ts.map +1 -0
  99. package/dist/core/ReasoningEvent.js +6 -0
  100. package/dist/core/ReasoningEvent.js.map +1 -0
  101. package/dist/core/ResourceMutations.d.ts +15 -19
  102. package/dist/core/ResourceMutations.d.ts.map +1 -1
  103. package/dist/core/ResourceMutations.js +118 -41
  104. package/dist/core/ResourceMutations.js.map +1 -1
  105. package/dist/core/SchemeRegistry.d.ts +12 -12
  106. package/dist/core/SchemeRegistry.d.ts.map +1 -1
  107. package/dist/core/SchemeRegistry.js +58 -57
  108. package/dist/core/SchemeRegistry.js.map +1 -1
  109. package/dist/core/ToolResources.d.ts +19 -0
  110. package/dist/core/ToolResources.d.ts.map +1 -0
  111. package/dist/core/ToolResources.js +123 -0
  112. package/dist/core/ToolResources.js.map +1 -0
  113. package/dist/core/Turn.d.ts +27 -0
  114. package/dist/core/Turn.d.ts.map +1 -0
  115. package/dist/core/Turn.js +43 -0
  116. package/dist/core/Turn.js.map +1 -0
  117. package/dist/core/Turn.sql +72 -0
  118. package/dist/core/TurnOps.d.ts +7 -0
  119. package/dist/core/TurnOps.d.ts.map +1 -0
  120. package/dist/core/TurnOps.js +73 -0
  121. package/dist/core/TurnOps.js.map +1 -0
  122. package/dist/core/TurnRunner.d.ts +56 -5
  123. package/dist/core/TurnRunner.d.ts.map +1 -1
  124. package/dist/core/TurnRunner.js +1163 -929
  125. package/dist/core/TurnRunner.js.map +1 -1
  126. package/dist/core/WorkerName.d.ts +2 -1
  127. package/dist/core/WorkerName.d.ts.map +1 -1
  128. package/dist/core/WorkerName.js +2 -1
  129. package/dist/core/WorkerName.js.map +1 -1
  130. package/dist/core/WorkerName.sql +17 -2
  131. package/dist/core/WorkspaceGate.js +1 -1
  132. package/dist/core/WorkspaceGate.js.map +1 -1
  133. package/dist/core/caps/CapsResolve.d.ts +2 -2
  134. package/dist/core/caps/CapsResolve.d.ts.map +1 -1
  135. package/dist/core/caps/CapsResolve.js +5 -7
  136. package/dist/core/caps/CapsResolve.js.map +1 -1
  137. package/dist/core/caps/DbChannelCaps.d.ts +1 -1
  138. package/dist/core/caps/DbChannelCaps.d.ts.map +1 -1
  139. package/dist/core/caps/DbChannelCaps.js +13 -11
  140. package/dist/core/caps/DbChannelCaps.js.map +1 -1
  141. package/dist/core/caps/DbEntryCaps.d.ts +5 -5
  142. package/dist/core/caps/DbEntryCaps.d.ts.map +1 -1
  143. package/dist/core/caps/DbEntryCaps.js +27 -30
  144. package/dist/core/caps/DbEntryCaps.js.map +1 -1
  145. package/dist/core/caps/DbNotifyCaps.d.ts +1 -1
  146. package/dist/core/caps/DbNotifyCaps.d.ts.map +1 -1
  147. package/dist/core/caps/DbNotifyCaps.js +5 -3
  148. package/dist/core/caps/DbNotifyCaps.js.map +1 -1
  149. package/dist/core/caps/DbSubscriptionCaps.d.ts +3 -3
  150. package/dist/core/caps/DbSubscriptionCaps.d.ts.map +1 -1
  151. package/dist/core/caps/DbSubscriptionCaps.js +16 -26
  152. package/dist/core/caps/DbSubscriptionCaps.js.map +1 -1
  153. package/dist/core/caps/SchemeCtxImpl.d.ts +6 -4
  154. package/dist/core/caps/SchemeCtxImpl.d.ts.map +1 -1
  155. package/dist/core/caps/SchemeCtxImpl.js +37 -5
  156. package/dist/core/caps/SchemeCtxImpl.js.map +1 -1
  157. package/dist/core/env-defaults.d.ts.map +1 -1
  158. package/dist/core/env-defaults.js +7 -5
  159. package/dist/core/env-defaults.js.map +1 -1
  160. package/dist/core/file-creation-policy.d.ts +9 -0
  161. package/dist/core/file-creation-policy.d.ts.map +1 -0
  162. package/dist/core/file-creation-policy.js +25 -0
  163. package/dist/core/file-creation-policy.js.map +1 -0
  164. package/dist/core/fork.d.ts +4 -1
  165. package/dist/core/fork.d.ts.map +1 -1
  166. package/dist/core/fork.js +39 -16
  167. package/dist/core/fork.js.map +1 -1
  168. package/dist/core/fork.sql +83 -40
  169. package/dist/core/git-env.d.ts +0 -1
  170. package/dist/core/git-env.d.ts.map +1 -1
  171. package/dist/core/git-env.js +0 -3
  172. package/dist/core/git-env.js.map +1 -1
  173. package/dist/core/git-membership.d.ts +19 -1
  174. package/dist/core/git-membership.d.ts.map +1 -1
  175. package/dist/core/git-membership.js +233 -74
  176. package/dist/core/git-membership.js.map +1 -1
  177. package/dist/core/git-state.d.ts.map +1 -1
  178. package/dist/core/git-state.js +13 -14
  179. package/dist/core/git-state.js.map +1 -1
  180. package/dist/core/operation-target-groups.d.ts +3 -0
  181. package/dist/core/operation-target-groups.d.ts.map +1 -0
  182. package/dist/core/operation-target-groups.js +53 -0
  183. package/dist/core/operation-target-groups.js.map +1 -0
  184. package/dist/core/packet-inject.d.ts +0 -1
  185. package/dist/core/packet-inject.d.ts.map +1 -1
  186. package/dist/core/packet-inject.js +6 -13
  187. package/dist/core/packet-inject.js.map +1 -1
  188. package/dist/core/packet-wire.d.ts +4 -0
  189. package/dist/core/packet-wire.d.ts.map +1 -1
  190. package/dist/core/packet-wire.js +226 -53
  191. package/dist/core/packet-wire.js.map +1 -1
  192. package/dist/core/plurnk-uri.d.ts +14 -3
  193. package/dist/core/plurnk-uri.d.ts.map +1 -1
  194. package/dist/core/plurnk-uri.js +62 -24
  195. package/dist/core/plurnk-uri.js.map +1 -1
  196. package/dist/core/scheme-types.d.ts +1 -0
  197. package/dist/core/scheme-types.d.ts.map +1 -1
  198. package/dist/core/teaching.d.ts.map +1 -1
  199. package/dist/core/teaching.js +4 -5
  200. package/dist/core/teaching.js.map +1 -1
  201. package/dist/core/worker-ops.sql +10 -6
  202. package/dist/core/worker-settings.d.ts +10 -0
  203. package/dist/core/worker-settings.d.ts.map +1 -0
  204. package/dist/core/worker-settings.js +32 -0
  205. package/dist/core/worker-settings.js.map +1 -0
  206. package/dist/core/workspace-settings.d.ts +2 -11
  207. package/dist/core/workspace-settings.d.ts.map +1 -1
  208. package/dist/core/workspace-settings.js +7 -47
  209. package/dist/core/workspace-settings.js.map +1 -1
  210. package/dist/digest/Digest.d.ts.map +1 -1
  211. package/dist/digest/Digest.js +164 -92
  212. package/dist/digest/Digest.js.map +1 -1
  213. package/dist/digest/digest.sql +9 -3
  214. package/dist/observe/genai.d.ts +9 -0
  215. package/dist/observe/genai.d.ts.map +1 -0
  216. package/dist/observe/genai.js +31 -0
  217. package/dist/observe/genai.js.map +1 -0
  218. package/dist/observe/spans.d.ts +3 -3
  219. package/dist/observe/spans.d.ts.map +1 -1
  220. package/dist/observe/spans.js +20 -5
  221. package/dist/observe/spans.js.map +1 -1
  222. package/dist/schemes/EffectPolicy.d.ts +3 -0
  223. package/dist/schemes/EffectPolicy.d.ts.map +1 -1
  224. package/dist/schemes/EffectPolicy.js +38 -6
  225. package/dist/schemes/EffectPolicy.js.map +1 -1
  226. package/dist/schemes/Exec.d.ts +3 -2
  227. package/dist/schemes/Exec.d.ts.map +1 -1
  228. package/dist/schemes/Exec.js +109 -34
  229. package/dist/schemes/Exec.js.map +1 -1
  230. package/dist/schemes/ExecOutputScheme.d.ts.map +1 -1
  231. package/dist/schemes/ExecOutputScheme.js +5 -4
  232. package/dist/schemes/ExecOutputScheme.js.map +1 -1
  233. package/dist/schemes/File.d.ts.map +1 -1
  234. package/dist/schemes/File.js +170 -80
  235. package/dist/schemes/File.js.map +1 -1
  236. package/dist/schemes/Log.d.ts +6 -2
  237. package/dist/schemes/Log.d.ts.map +1 -1
  238. package/dist/schemes/Log.js +73 -22
  239. package/dist/schemes/Log.js.map +1 -1
  240. package/dist/schemes/Log.sql +25 -7
  241. package/dist/schemes/Prompt.d.ts +2 -3
  242. package/dist/schemes/Prompt.d.ts.map +1 -1
  243. package/dist/schemes/Prompt.js +5 -9
  244. package/dist/schemes/Prompt.js.map +1 -1
  245. package/dist/schemes/QuestionTool.d.ts +10 -0
  246. package/dist/schemes/QuestionTool.d.ts.map +1 -0
  247. package/dist/schemes/QuestionTool.js +81 -0
  248. package/dist/schemes/QuestionTool.js.map +1 -0
  249. package/dist/schemes/Skill.d.ts.map +1 -1
  250. package/dist/schemes/Skill.js +6 -1
  251. package/dist/schemes/Skill.js.map +1 -1
  252. package/dist/schemes/Worker.d.ts.map +1 -1
  253. package/dist/schemes/Worker.js +97 -34
  254. package/dist/schemes/Worker.js.map +1 -1
  255. package/dist/schemes/_entry-crud.d.ts +5 -5
  256. package/dist/schemes/_entry-crud.d.ts.map +1 -1
  257. package/dist/schemes/_entry-crud.js +22 -17
  258. package/dist/schemes/_entry-crud.js.map +1 -1
  259. package/dist/schemes/_entry-crud.sql +77 -24
  260. package/dist/schemes/_entry-find.d.ts +8 -5
  261. package/dist/schemes/_entry-find.d.ts.map +1 -1
  262. package/dist/schemes/_entry-find.js +121 -40
  263. package/dist/schemes/_entry-find.js.map +1 -1
  264. package/dist/schemes/_entry-find.sql +16 -11
  265. package/dist/schemes/_entry-graph.sql +2 -6
  266. package/dist/schemes/_entry-manifest.d.ts +2 -2
  267. package/dist/schemes/_entry-manifest.d.ts.map +1 -1
  268. package/dist/schemes/_entry-manifest.js +18 -6
  269. package/dist/schemes/_entry-manifest.js.map +1 -1
  270. package/dist/schemes/_entry-ops.d.ts +6 -5
  271. package/dist/schemes/_entry-ops.d.ts.map +1 -1
  272. package/dist/schemes/_entry-ops.js +39 -46
  273. package/dist/schemes/_entry-ops.js.map +1 -1
  274. package/dist/schemes/_entry-ops.sql +9 -6
  275. package/dist/schemes/_entry-send.d.ts +2 -2
  276. package/dist/schemes/_entry-send.d.ts.map +1 -1
  277. package/dist/schemes/_entry-send.js +16 -14
  278. package/dist/schemes/_entry-send.js.map +1 -1
  279. package/dist/schemes/_search-index.d.ts.map +1 -1
  280. package/dist/schemes/_search-index.js +39 -25
  281. package/dist/schemes/_search-index.js.map +1 -1
  282. package/dist/server/Daemon.d.ts +98 -15
  283. package/dist/server/Daemon.d.ts.map +1 -1
  284. package/dist/server/Daemon.js +864 -348
  285. package/dist/server/Daemon.js.map +1 -1
  286. package/dist/server/DaemonModule.d.ts +84 -19
  287. package/dist/server/DaemonModule.d.ts.map +1 -1
  288. package/dist/server/DrainSupervisor.d.ts +15 -10
  289. package/dist/server/DrainSupervisor.d.ts.map +1 -1
  290. package/dist/server/DrainSupervisor.js +70 -19
  291. package/dist/server/DrainSupervisor.js.map +1 -1
  292. package/dist/server/Functionality.d.ts +44 -0
  293. package/dist/server/Functionality.d.ts.map +1 -0
  294. package/dist/server/Functionality.js +422 -0
  295. package/dist/server/Functionality.js.map +1 -0
  296. package/dist/server/FunctionalityManager.d.ts +22 -0
  297. package/dist/server/FunctionalityManager.d.ts.map +1 -0
  298. package/dist/server/FunctionalityManager.js +121 -0
  299. package/dist/server/FunctionalityManager.js.map +1 -0
  300. package/dist/server/SkillsFunctionality.d.ts +61 -0
  301. package/dist/server/SkillsFunctionality.d.ts.map +1 -0
  302. package/dist/server/SkillsFunctionality.js +449 -0
  303. package/dist/server/SkillsFunctionality.js.map +1 -0
  304. package/dist/server/WorkerCapabilities.d.ts +21 -0
  305. package/dist/server/WorkerCapabilities.d.ts.map +1 -0
  306. package/dist/server/WorkerCapabilities.js +214 -0
  307. package/dist/server/WorkerCapabilities.js.map +1 -0
  308. package/dist/server/WorkerResidency.d.ts +40 -0
  309. package/dist/server/WorkerResidency.d.ts.map +1 -0
  310. package/dist/server/WorkerResidency.js +283 -0
  311. package/dist/server/WorkerResidency.js.map +1 -0
  312. package/dist/server/client-input.d.ts +6 -2
  313. package/dist/server/client-input.d.ts.map +1 -1
  314. package/dist/server/client-input.js +72 -32
  315. package/dist/server/client-input.js.map +1 -1
  316. package/dist/server/dispatch-as-plurnk.d.ts +2 -2
  317. package/dist/server/dispatch-as-plurnk.d.ts.map +1 -1
  318. package/dist/server/dispatch-as-plurnk.js +109 -23
  319. package/dist/server/dispatch-as-plurnk.js.map +1 -1
  320. package/dist/server/drain.sql +33 -13
  321. package/dist/server/envelope.d.ts +7 -2
  322. package/dist/server/envelope.d.ts.map +1 -1
  323. package/dist/server/envelope.js +12 -6
  324. package/dist/server/envelope.js.map +1 -1
  325. package/dist/server/envelope.sql +36 -4
  326. package/dist/server/lifecycle-recovery.sql +24 -17
  327. package/dist/server/logEntry.d.ts +2 -1
  328. package/dist/server/logEntry.d.ts.map +1 -1
  329. package/dist/server/logEntry.js +3 -2
  330. package/dist/server/logEntry.js.map +1 -1
  331. package/dist/server/logEntry.sql +7 -0
  332. package/dist/server/loop-model.d.ts +2 -2
  333. package/dist/server/loop-model.d.ts.map +1 -1
  334. package/dist/server/loop-model.js +23 -33
  335. package/dist/server/loop-model.js.map +1 -1
  336. package/dist/server/loopDocs.d.ts +2 -1
  337. package/dist/server/loopDocs.d.ts.map +1 -1
  338. package/dist/server/loopDocs.js +88 -21
  339. package/dist/server/loopDocs.js.map +1 -1
  340. package/dist/server/loopDocs.sql +14 -8
  341. package/dist/server/model-catalog.d.ts +3 -0
  342. package/dist/server/model-catalog.d.ts.map +1 -0
  343. package/dist/server/model-catalog.js +83 -0
  344. package/dist/server/model-catalog.js.map +1 -0
  345. package/dist/server/model-route.d.ts +7 -0
  346. package/dist/server/model-route.d.ts.map +1 -0
  347. package/dist/server/model-route.js +47 -0
  348. package/dist/server/model-route.js.map +1 -0
  349. package/dist/server/module-discovery.d.ts +13 -0
  350. package/dist/server/module-discovery.d.ts.map +1 -0
  351. package/dist/server/module-discovery.js +103 -0
  352. package/dist/server/module-discovery.js.map +1 -0
  353. package/dist/server/seam-entry-read.sql +9 -7
  354. package/dist/server/seam-log-read.sql +1 -1
  355. package/dist/server/seam-loop.sql +10 -1
  356. package/dist/server/seam-proposal-list.sql +4 -2
  357. package/dist/server/worker-capabilities.sql +19 -0
  358. package/dist/service.d.ts.map +1 -1
  359. package/dist/service.js +193 -124
  360. package/dist/service.js.map +1 -1
  361. package/migrations/001_schema.sql +905 -196
  362. package/package.json +40 -38
  363. package/dist/core/ExecutableTools.d.ts +0 -11
  364. package/dist/core/ExecutableTools.d.ts.map +0 -1
  365. package/dist/core/ExecutableTools.js +0 -49
  366. package/dist/core/ExecutableTools.js.map +0 -1
  367. package/dist/core/JournalTurn.d.ts +0 -9
  368. package/dist/core/JournalTurn.d.ts.map +0 -1
  369. package/dist/core/JournalTurn.js +0 -14
  370. package/dist/core/JournalTurn.js.map +0 -1
  371. package/dist/core/JournalTurn.sql +0 -10
  372. package/dist/core/git-iso.d.ts +0 -13
  373. package/dist/core/git-iso.d.ts.map +0 -1
  374. package/dist/core/git-iso.js +0 -267
  375. package/dist/core/git-iso.js.map +0 -1
  376. package/dist/core/search-gate.d.ts +0 -17
  377. package/dist/core/search-gate.d.ts.map +0 -1
  378. package/dist/core/search-gate.js +0 -77
  379. package/dist/core/search-gate.js.map +0 -1
  380. package/dist/digest/Digest.sql +0 -1
  381. package/dist/server/workspace-capabilities.sql +0 -19
@@ -1,7 +1,6 @@
1
1
  var _a;
2
- import { PlurnkParser, PlurnkParseError, UNKNOWN_POSITION } from "@plurnk/plurnk-contracts";
3
- import Owner from "./Owner.js";
4
- const TERMINAL_SEND_SIGNALS = new Set([102, 200, 202, 300, 499]);
2
+ import { PathSyntax, PlurnkParser, PlurnkParseError, UNKNOWN_POSITION } from "@plurnk/plurnk-contracts";
3
+ const TERMINAL_SEND_SIGNALS = new Set([102, 200, 202, 499]);
5
4
  const comparePosition = (a, b) => a.line - b.line || a.column - b.column;
6
5
  import Meta from "@plurnk/plurnk-meta";
7
6
  import EntryCrud from "../schemes/_entry-crud.js";
@@ -9,8 +8,16 @@ import GitMembership from "./git-membership.js";
9
8
  import GitState from "./git-state.js";
10
9
  import WorkspaceSettings from "./workspace-settings.js";
11
10
  import { editedSpan } from "../content/index.js";
12
- import { promptPathname, promptLoopPrefix, renderTarget } from "./plurnk-uri.js";
11
+ import { authorityParts, generatedPathname, promptPathname, promptLoopPrefix, renderAddress } from "./plurnk-uri.js";
13
12
  import { readFile } from "node:fs/promises";
13
+ // {§grammar-rail-registration} — bare names are the built-in rail namespace;
14
+ // everything else is an import specifier resolved through the Node chain.
15
+ export const resolveGrammarRailPath = (variant) => variant.startsWith("/") || variant.startsWith(".")
16
+ || variant.includes("/") || variant.includes(":")
17
+ ? (variant.startsWith("/") || variant.startsWith(".")
18
+ ? variant
19
+ : fileURLToPath(import.meta.resolve(variant)))
20
+ : fileURLToPath(import.meta.resolve(`@plurnk/plurnk-contracts/${variant}`));
14
21
  import { fileURLToPath } from "node:url";
15
22
  import { randomUUID } from "node:crypto";
16
23
  // Shared module imported by both Engine and bin/digest.ts, so wire
@@ -20,16 +27,22 @@ import PacketWire from "./packet-wire.js";
20
27
  import Results, { OperationFailureError } from "./results.js";
21
28
  import BranchReceipt from "./BranchReceipt.js";
22
29
  import TerminalResult from "./TerminalResult.js";
30
+ import LoopLifecycle from "./LoopLifecycle.js";
23
31
  import WorkerControlAddress from "./WorkerControlAddress.js";
24
- import JournalTurn from "./JournalTurn.js";
32
+ import Turn from "./Turn.js";
25
33
  import LogBody from "./LogBody.js";
34
+ import LogVisibility from "./LogVisibility.js";
26
35
  import StrikeRail from "./StrikeRail.js";
27
36
  import StoredPacket from "./StoredPacket.js";
28
37
  import { observed, observedSync } from "../observe/spans.js";
38
+ import { GEN_AI_REQUEST_SPAN, genAiRequestOptions, settleGenAiResponse, } from "../observe/genai.js";
29
39
  import { OPS_DISPATCHED, PROVIDER_CALLS, recordCounter } from "../observe/metrics.js";
30
40
  import { scheduleTurnOps } from "./turn-scheduler.js";
41
+ import { expandSafeUriTargetGroup } from "./operation-target-groups.js";
31
42
  import { readOptimisticSettlementMs } from "./optimistic-settlement.js";
32
43
  import ModelCall, { ModelCallPersistenceError, ProviderAccountingIntegrityError, } from "./ModelCall.js";
44
+ import OverflowTurn from "./OverflowTurn.js";
45
+ import TurnOps from "./TurnOps.js";
33
46
  const RECORD_STREAM_MIMETYPES = new Set(["application/jsonl", "application/x-ndjson"]);
34
47
  const baseMimetype = (mimetype) => mimetype.split(";", 1)[0].trim().toLowerCase();
35
48
  // {§exec-stream} Active streams publish only independently meaningful units.
@@ -56,8 +69,8 @@ const ENGINE_PROBLEMS = Object.freeze({
56
69
  });
57
70
  // The prompt entry target - prompt:///<loop>/<N>, self-only ({§prompt-self-only}):
58
71
  // the owner rides the owner_id column, the address carries only the loop coordinate.
59
- const promptTarget = (loopSeq, turnSeq) => {
60
- const storage = promptPathname(loopSeq, turnSeq);
72
+ const promptTarget = (loopSeq, promptOrdinal) => {
73
+ const storage = promptPathname(loopSeq, promptOrdinal);
61
74
  return {
62
75
  kind: "url", raw: `prompt://${storage}`,
63
76
  scheme: "prompt", username: null, password: null,
@@ -78,6 +91,15 @@ const parsePromptAttributes = (encoded, source) => {
78
91
  }
79
92
  return attributes;
80
93
  };
94
+ const promptSourceFromAttributes = (attributes, source) => {
95
+ const value = attributes.source;
96
+ if (value === undefined)
97
+ return null;
98
+ if (typeof value !== "string" || value.length === 0) {
99
+ throw new TypeError(`${source}: source must be a non-empty string when present`);
100
+ }
101
+ return value;
102
+ };
81
103
  // Per-emission action ceiling — OFF by default. `-1` (or unset/non-positive) = no cap:
82
104
  // every generated op dispatches. Runaway degeneration is a sampler concern (repetition penalty),
83
105
  // not grounds to drop already-generated work. A positive value is an operator ceiling a
@@ -101,7 +123,7 @@ const readFilesItems = () => {
101
123
  return null;
102
124
  return normalizeFilesItems(Number.parseInt(raw, 10));
103
125
  };
104
- import { ProviderError, resolveActiveAlias, scopeEnvToAlias, } from "@plurnk/plurnk-providers";
126
+ import { ProviderError, scopeEnvToAlias, } from "@plurnk/plurnk-providers";
105
127
  import { validateGbnf } from "@plurnk/gbnf";
106
128
  import ProviderInstantiate from "./ProviderInstantiate.js";
107
129
  // A generated rail may constrain only the sampled continuation while the chat
@@ -126,8 +148,8 @@ const grammarConstraint = (grammar) => {
126
148
  response: grammar.replace(roots[0][0], `root ::= ${responseRoot}`),
127
149
  };
128
150
  };
129
- const TOKEN_BUDGET_OVERFLOW_DETAIL = "Token Budget Overflow: Token Usage exceeded Token Ceiling. Newest log items were automatically FOLDed to fit within token budget. Curate the log and/or perform more conservatively scoped or chunked retrieval operations to recover.";
130
- const curationOverflowFailure = (pressure) => Results.failure("engine:context", "token-budget-overflow", 413, TOKEN_BUDGET_OVERFLOW_DETAIL, {}, {
151
+ const TOKEN_BUDGET_OVERFLOW_HARD_DETAIL = "Context Token Budget Overflow: tokensActiveTotal exceeded tokensActiveMax, and the causal overflow turn could not make enough room.";
152
+ const curationOverflowFailure = (pressure) => Results.failure("engine:context", "token-budget-overflow", 413, TOKEN_BUDGET_OVERFLOW_HARD_DETAIL, {}, {
131
153
  usage: pressure.weight,
132
154
  ceiling: pressure.budget,
133
155
  deficit: pressure.excess,
@@ -159,6 +181,7 @@ class TurnRunner {
159
181
  #dispatcher;
160
182
  #liveSubscriptions;
161
183
  #streamEventNotify;
184
+ #reasoningEventNotify;
162
185
  #wakeWorkerNotify;
163
186
  #executors;
164
187
  #loopSignal;
@@ -167,7 +190,7 @@ class TurnRunner {
167
190
  #dispatch;
168
191
  #resolveWorkerProviderIdentity;
169
192
  #gbnfCache = new Map();
170
- constructor({ db, schemes, mimetypes, weigh, notices, problems, strikes, packets, dispatcher, liveSubscriptions, streamEventNotify, wakeWorkerNotify, executors, loopSignal, interactions, warmWorkspace, dispatch, resolveWorkerProviderIdentity, }) {
193
+ constructor({ db, schemes, mimetypes, weigh, notices, problems, strikes, packets, dispatcher, liveSubscriptions, streamEventNotify, reasoningEventNotify, wakeWorkerNotify, executors, loopSignal, interactions, warmWorkspace, dispatch, resolveWorkerProviderIdentity, }) {
171
194
  this.#db = db;
172
195
  this.#schemes = schemes;
173
196
  this.#mimetypes = mimetypes;
@@ -179,6 +202,7 @@ class TurnRunner {
179
202
  this.#dispatcher = dispatcher;
180
203
  this.#liveSubscriptions = liveSubscriptions;
181
204
  this.#streamEventNotify = streamEventNotify;
205
+ this.#reasoningEventNotify = reasoningEventNotify;
182
206
  this.#wakeWorkerNotify = wakeWorkerNotify;
183
207
  this.#executors = executors;
184
208
  this.#loopSignal = loopSignal;
@@ -196,7 +220,7 @@ class TurnRunner {
196
220
  _a.#railGapWarned.add(message);
197
221
  process.stderr.write(`plurnk-engine: rail verdict unavailable — the configured grammar did not parse in @plurnk/gbnf (${message})\n`);
198
222
  }
199
- static #requireGrammarEvidence(response) {
223
+ static #requireGrammarEvidence(response, allowWithheld) {
200
224
  const evidence = response.grammarEvidence;
201
225
  if (evidence === undefined) {
202
226
  throw new Error("provider contract violation: configured GBNF response omitted grammar evidence");
@@ -209,32 +233,42 @@ class TurnRunner {
209
233
  || input.slice(evidence.contentStart).join("") !== response.assistant.content) {
210
234
  throw new Error("provider contract violation: grammar evidence does not map exactly to assistant.content");
211
235
  }
236
+ if (!allowWithheld && !evidence.transported) {
237
+ throw new Error("provider contract violation: configured GBNF was not transported outside explicit debug mode");
238
+ }
212
239
  return evidence;
213
240
  }
214
241
  async #grammarConstraint(provider) {
215
- // Resolve through the registered or active alias; ambiguity and load
216
- // failures never degrade to unconstrained generation.
217
- // {§grammar-enforcement-verified-at-boot}
218
- const registered = ProviderInstantiate.aliasOf(provider);
219
- const fallback = registered === undefined ? resolveActiveAlias(process.env)?.alias : undefined;
220
- if (registered === undefined && fallback === undefined && Object.keys(process.env).some((k) => k.startsWith("PLURNK_PROVIDERS_GBNF_"))) {
221
- throw new Error("GBNF constraint: provider has no registered alias and no active alias resolves, while per-alias PLURNK_PROVIDERS_GBNF_* constraints are configured");
222
- }
223
- const alias = registered ?? fallback ?? "";
224
- const variant = scopeEnvToAlias(process.env, alias, ["PLURNK_PROVIDERS_GBNF"]).PLURNK_PROVIDERS_GBNF;
242
+ // A real alias scopes its rail; an alias-free direct route uses the
243
+ // global provider configuration. Unrelated alias settings never apply.
244
+ // {§grammar-configuration-admission}
245
+ ProviderInstantiate.assertGrammarConfigurationScope(provider);
246
+ const alias = ProviderInstantiate.configurationAliasOf(provider);
247
+ const scoped = alias === undefined
248
+ ? process.env
249
+ : scopeEnvToAlias(process.env, alias, [
250
+ "PLURNK_PROVIDERS_GBNF",
251
+ "PLURNK_PROVIDERS_GBNF_DEBUG",
252
+ ]);
253
+ const variant = scoped.PLURNK_PROVIDERS_GBNF;
225
254
  if (variant === undefined || variant === "" || variant === "0")
226
255
  return undefined;
256
+ const allowWithheld = scoped.PLURNK_PROVIDERS_GBNF_DEBUG !== undefined
257
+ && scoped.PLURNK_PROVIDERS_GBNF_DEBUG !== ""
258
+ && scoped.PLURNK_PROVIDERS_GBNF_DEBUG !== "0";
227
259
  const hit = this.#gbnfCache.get(variant);
228
260
  if (hit !== undefined)
229
- return hit;
230
- const path = variant.startsWith("/") || variant.startsWith(".")
231
- ? variant
232
- : fileURLToPath(import.meta.resolve(`@plurnk/plurnk-contracts/${variant}`));
261
+ return { ...hit, allowWithheld };
262
+ // {§grammar-rail-registration} — a bare name is a built-in rail subpath
263
+ // under @plurnk/plurnk-contracts; any other form is an import specifier
264
+ // (an operator file path or a package export subpath), so a third-party
265
+ // rail package plugs in with no built-in registry change.
266
+ const path = resolveGrammarRailPath(variant);
233
267
  const text = await readFile(path, "utf8"); // unresolvable/unreadable throws — a configured rail never silently degrades
234
268
  const constraint = grammarConstraint(text);
235
269
  this.#gbnfCache.set(variant, constraint);
236
270
  process.stderr.write(`plurnk-engine: GBNF constraint: ${alias || "(bare)"} → ${variant} (${text.length} chars)\n`);
237
- return constraint;
271
+ return { ...constraint, allowWithheld };
238
272
  }
239
273
  // A lineage's no-parent root; a root worker resolves to itself. Fail hard
240
274
  // when corruption leaves a worker without one. {§worker-primary}
@@ -282,7 +316,7 @@ class TurnRunner {
282
316
  }
283
317
  const settlements = await Promise.allSettled(prepared.map(async ({ statement, modelCall, attributions, providerWorkerId }) => {
284
318
  try {
285
- const response = await observed("provider.generate", { model: provider.model, attempt: 1, kind: "bare" }, async (span) => {
319
+ const response = await observed(GEN_AI_REQUEST_SPAN, { model: provider.model, attempt: 1, kind: "bare" }, async (span) => {
286
320
  try {
287
321
  const generated = await provider.generate({
288
322
  messages: [{ role: "user", content: statement.body }],
@@ -303,6 +337,7 @@ class TurnRunner {
303
337
  status: "resolved",
304
338
  });
305
339
  span.setAttribute("status", "resolved");
340
+ settleGenAiResponse(span, generated);
306
341
  return generated;
307
342
  }
308
343
  catch (error) {
@@ -311,7 +346,7 @@ class TurnRunner {
311
346
  }
312
347
  throw error;
313
348
  }
314
- });
349
+ }, genAiRequestOptions(ProviderInstantiate.aliasOf(provider) ?? "plurnk", provider.model));
315
350
  await modelCall.observeResponse(response);
316
351
  return {
317
352
  statement,
@@ -361,777 +396,44 @@ class TurnRunner {
361
396
  }
362
397
  return { line, column };
363
398
  }
364
- async runTurn({ provider, childProvider = provider, messages, requirements = "", workspaceId, workerId, loopId, origin = "model", signal, onDispatch, turnNumber = 1, maxTurns = 50, invalidEmissionRecoveryEntryId, }) {
365
- const allowInvalidEmissionRecovery = invalidEmissionRecoveryEntryId === null;
366
- const transientOpenLogEntryId = typeof invalidEmissionRecoveryEntryId === "number"
367
- ? invalidEmissionRecoveryEntryId
368
- : null;
369
- // === Turn-as-container model ===
370
- //
371
- // Turn rows are created at runTurn OPEN (status=102, placeholder
372
- // packet) so things can be written into the turn before the model
373
- // is called: the user prompt on turn 1; later, system signals or
374
- // injected Notices on any turn. The turn is CLOSED at
375
- // the end with the final packet + status + usage stats.
376
- //
377
- // sequence is "ordinal of stuff in this turn." Pre-model
378
- // writes consume low indices; model ops continue from there.
379
- const seqRow = await this.#db.engine_next_turn_sequence.get({ loop_id: loopId });
380
- const seq = seqRow.next;
381
- // loops.sequence is the loop's ordinal within the worker. Turn-0 foists that belong to the
382
- // WORKER (manifest preview, AGENTS, operator docs) gate on the worker's first loop, not every loop's
383
- // first turn ({§actor-boundary-catalog-preview}); per-loop foists such as
384
- // {§prompt-entry} still fire each loop. Read once, turn-1 only.
385
- const loopRow = seq === 1
386
- ? await this.#db.engine_get_loop_prompt.get({ loop_id: loopId })
387
- : undefined;
388
- const workerFirstLoop = (loopRow?.sequence ?? 0) === 1;
389
- const openRow = await this.#db.engine_open_turn.get({
390
- loop_id: loopId, sequence: seq,
391
- });
392
- if (openRow === undefined)
393
- throw new Error("Engine.runTurn: turn open returned no row");
394
- const turnId = openRow.id;
395
- // {§env-delta-log-pull} — establish a fresh worker's observation
396
- // baseline immediately after its first turn opens. A fork already has
397
- // its parent's cursor, so the NULL-guarded statement leaves it intact.
398
- // Events committed after this statement belong to this or a later
399
- // closed pull window; pre-existing state is read through the ordinary
400
- // shared-world projections in this first packet.
401
- await this.#db.engine_initialize_ambient_cursor.get({ workspace_id: workspaceId, worker_id: workerId });
402
- // Threaded per turn, never engine state, so concurrent loops on
403
- // different providers each read their own honest tokenizer values.
404
- const systemCtx = {
405
- db: this.#db, workspaceId, workerId, loopId, turnId,
406
- writer: "plurnk",
407
- signal: this.#loopSignal(loopId),
408
- streamEventNotify: this.#streamEventNotify,
409
- wakeWorkerNotify: this.#wakeWorkerNotify,
410
- weigh: this.#weighContent,
411
- mimetypes: this.#mimetypes,
412
- defaultChannelFor: (s) => this.#schemes.defaultChannelFor(s, workspaceId),
413
- pushNotice: (notice) => this.#notices.push(workspaceId, loopId, notice),
414
- requestInteraction: (request) => this.#interactions.request(request, { workspaceId, workerId, loopId, turnId }, this.#loopSignal(loopId)),
415
- };
416
- // Pre-model writes. Each prompt the model has not seen yet becomes an
417
- // actionless `prompt` log row whose target is its durable prompt:// entry:
418
- // - Turn 1: loop.prompt is the initial user prompt.
419
- // - Turn N>1: only if Engine.inject (or wake-on-completion via
420
- // daemon.inject) wrote a prompt entry for this turn slot
421
- // between turn N-1 and N. Inject writes directly to entries;
422
- // we DON'T re-foist here for N>1.
423
- // Model ops dispatch after these pre-model rows.
424
- let nextActionIndex = 1;
425
- const turnOpenPaths = [];
426
- // {§worker-initialization-entry} — the worker's first turn opens with a kernel-authored initialization: a
427
- // worked turn PLAN → the environment FINDs the foist ACTUALLY dispatches → SEND signal 102. Built
428
- // from the real ops below (not a static print — we lean into the genuine echo paradigm) and
429
- // written at sequence 1, so it reads first with the foisted results following.
430
- const turnZeroMoves = [];
431
- if (seq === 1) {
432
- if (workerFirstLoop)
433
- nextActionIndex = 2; // reserve sequence 1 for initialization
434
- // Operator doc READs (PLURNK_SERVICE_MD_<ALIAS>, {§actor-boundary-doc-injection}). The docs were materialized
435
- // as worker://plurnk/<entry> entries by the plurnk worker (LoopDocs, via the
436
- // {§actor-boundary} keystone); foist a READ of each into THIS turn-0 so the model
437
- // reads them inline. It sees only the READ — the materializing EDIT
438
- // lives in the plurnk worker's log, never the model's.
439
- // {§operator-config-workspace-md-docs} — env docs union the workspace's client docs; foist a READ of
440
- // each materialized worker://plurnk/<alias>.md (LoopDocs materialized the same set).
441
- const { mdDocs } = await WorkspaceSettings.read(this.#db, workspaceId);
442
- // {§actor-boundary-doc-injection} — operator docs appear on the worker's first loop.
443
- for (const doc of workerFirstLoop ? await WorkspaceSettings.resolveDocs(mdDocs) : []) {
444
- const docTarget = {
445
- kind: "url", raw: `worker://plurnk/${doc.entryName}`, scheme: "worker",
446
- username: null, password: null, hostname: "plurnk", port: null,
447
- pathname: `/${doc.entryName}`, query: null, fragment: null,
448
- };
449
- const docRead = {
450
- op: "READ", suffix: "", signal: null, target: docTarget,
451
- lineMarker: null, body: null, position: UNKNOWN_POSITION,
452
- };
453
- await this.#dispatch({
454
- statement: docRead, workspaceId, workerId, loopId, turnId,
455
- sequence: nextActionIndex, origin: "plurnk", onDispatch,
456
- });
457
- nextActionIndex++;
458
- }
459
- const promptRow = loopRow; // {§prompt-entry} — per-loop; fires every loop's turn 1
460
- if (promptRow !== undefined && typeof promptRow.prompt === "string" && promptRow.prompt.length > 0) {
461
- const openPaths = assertOpenPaths(JSON.parse(promptRow.open_paths), `Loop ${loopId} open_paths`);
462
- const promptLoopSeq = promptRow.sequence; // the loop's per-worker sequence — model-facing, matching log coordinates (owner: the db id read as prompt/2/1)
463
- const promptPath = promptTarget(promptLoopSeq, seq);
464
- const entry = {
465
- channels: { body: { content: promptRow.prompt, mimetype: "text/markdown" } },
466
- attributes: { openPaths },
467
- };
468
- await EntryCrud.writeEntry(promptPath.pathname, entry, systemCtx, "prompt", workerId);
469
- turnOpenPaths.push(...openPaths);
470
- const promptLogId = await this.#writePromptLog({
471
- workerId,
472
- loopId,
473
- turnId,
474
- sequence: nextActionIndex,
475
- target: promptPath,
476
- content: promptRow.prompt,
477
- });
478
- onDispatch?.(promptLogId);
479
- nextActionIndex++;
480
- }
481
- }
482
- // {§prompt-loop-containment}: the loop contains every prompt that arrived
483
- // while it ran. Publish each undelivered frame as a prompt row, oldest
484
- // first, so rapid arrivals reach the model together exactly once.
485
- {
486
- const loopSeqRow = await this.#db.engine_loop_sequence.get({ loop_id: loopId });
487
- const loopSeq = loopSeqRow?.sequence ?? loopId;
488
- const prefix = promptLoopPrefix(loopSeq);
489
- const undelivered = (await this.#db.drain_undelivered_prompts_for_loop.all({
490
- owner_id: workerId,
491
- pattern: `${prefix}%`,
492
- prefix_len: prefix.length,
493
- loop_id: loopId,
494
- }))
495
- .filter((r) => typeof r.content === "string" && r.content.length > 0);
496
- for (const injectedRow of undelivered) {
497
- const attributes = parsePromptAttributes(injectedRow.attributes, `Prompt ${injectedRow.pathname} attributes`);
498
- const encodedOpenPaths = attributes.openPaths;
499
- if (encodedOpenPaths !== undefined) {
500
- turnOpenPaths.push(...assertOpenPaths(encodedOpenPaths, `Prompt ${injectedRow.pathname} openPaths`));
501
- }
502
- const ordinal = Number(injectedRow.pathname.split("/").filter(Boolean).at(-1));
503
- const injTarget = promptTarget(loopSeq, ordinal);
504
- const promptLogId = await this.#writePromptLog({
505
- workerId,
506
- loopId,
507
- turnId,
508
- sequence: nextActionIndex,
509
- target: injTarget,
510
- content: injectedRow.content,
511
- });
512
- onDispatch?.(promptLogId);
513
- nextActionIndex++;
514
- }
515
- }
516
- // The persistent search-index pass (_search-index.maintain) attaches
517
- // every readable entry/log projection to complete graph/FTS/vector derivations.
518
- // NOT an action: no log entry, no sequence slot,
519
- // not dispatched. There is no materialized manifest entry — the catalog
520
- // is served on demand by FIND: recursive when asked, shallow-mapped below.
521
- // {§semantic-embed-dedup} — one pass-wide semantic plan binds every
522
- // chunk counter to the derivation identity it produces.
523
- // SPEC {§membership} D4/D5 — git-ls-files workspace membership, resolved at
524
- // prompt-composition (EMI is eager + exhaustive — git is the only bound). When the
525
- // workspace's project_root is a git working tree, tracked files are
526
- // members without a client `add`; active members are materialized
527
- // (disk → body channel) so they appear in the catalog. No-ops
528
- // on headless / non-git workspaces. Runs BEFORE the derivation pump so
529
- // this turn's packet reflects them.
530
- // Workspace creation starts this eagerly. Joining here is the correctness
531
- // boundary: the model never runs against partial graph/vector coverage.
532
- await this.#warmWorkspace(systemCtx, false);
533
- // The warm materialized membership before deriving. This second pass is the
534
- // ordinary cheap change detector and captures any drift that landed meanwhile.
535
- const fsDivergences = await GitMembership.indexGitMembership(systemCtx);
536
- // {§packet-git-status} — one post-reconciliation snapshot supplies both
537
- // the compact packet summary and each causal file event's exact XY state.
538
- const gitStatus = await GitState.status(this.#db, workspaceId, this.#loopSignal(loopId));
539
- await this.#logFsFictions(workspaceId, fsDivergences, gitStatus);
540
- // The refresh above may have changed bodies (including model/client edits
541
- // since the startup warm). Re-derive to completion before packet/model
542
- // construction. Membership is already current, so this pass does not
543
- // consume the filesystem divergences a second time.
544
- await this.#warmWorkspace(systemCtx, true, false);
545
- // Turn-0 catalog preview (PLURNK_SERVICE_FILES_ITEMS, {§actor-boundary-catalog-preview}):
546
- // Four FIND surveys foisted into the worker's first model turn establish the project,
547
- // commons, private, and documentation surfaces in that order. Their `init`
548
- // classification lets the model curate this opening survey as one log set.
549
- if (seq === 1) {
550
- // {§operator-config-workspace-files-items} — workspace filesItems replaces the env default.
551
- const { filesItems: workspaceMI } = await WorkspaceSettings.read(this.#db, workspaceId);
552
- const filesItems = workspaceMI !== null ? normalizeFilesItems(workspaceMI) : readFilesItems();
553
- if (filesItems !== null && workerFirstLoop) { // {§actor-boundary-catalog-preview} — once per worker
554
- const catalogSchemes = await this.#db.engine_scheme_catalog_summary.all({ workspace_id: workspaceId });
555
- const fileItems = catalogSchemes.find(({ scheme }) => scheme === "file")?.shallow_items ?? 0;
556
- const fileCap = filesItems > 0 && fileItems > 0 ? Math.min(filesItems, fileItems) : null;
557
- await Owner.kernelId(this.#db, workspaceId);
558
- const surveys = [
559
- {
560
- statement: {
561
- op: "FIND", suffix: "", signal: ["+init"],
562
- target: { kind: "local", raw: "*" },
563
- body: null,
564
- lineMarker: fileCap === null ? null : { marks: [1, fileCap] },
565
- position: UNKNOWN_POSITION,
566
- },
567
- exemplar: `## FIND0 [+init] (*)${fileCap === null ? "" : ` <1,${fileCap}>`}`,
568
- },
569
- {
570
- statement: {
571
- op: "FIND", suffix: "", signal: ["+init"],
572
- target: { kind: "url", raw: "worker:///*", scheme: "worker", username: null, password: null, hostname: null, port: null, pathname: "/*", query: null, fragment: null },
573
- body: null, lineMarker: null, position: UNKNOWN_POSITION,
574
- },
575
- exemplar: "## FIND0 [+init] (worker:///*)",
576
- },
577
- {
578
- statement: {
579
- op: "FIND", suffix: "", signal: ["+init"],
580
- target: { kind: "url", raw: "worker://~/*", scheme: "worker", username: null, password: null, hostname: "~", port: null, pathname: "/*", query: null, fragment: null },
581
- body: null, lineMarker: null, position: UNKNOWN_POSITION,
582
- },
583
- exemplar: "## FIND0 [+init] (worker://~/*)",
584
- },
585
- {
586
- statement: {
587
- op: "FIND", suffix: "", signal: ["+init", "+docs"],
588
- target: { kind: "url", raw: "worker://plurnk/docs/**", scheme: "worker", username: null, password: null, hostname: "plurnk", port: null, pathname: "/docs/**", query: null, fragment: null },
589
- body: null, lineMarker: { marks: [1, -1] }, position: UNKNOWN_POSITION,
590
- },
591
- exemplar: "## FIND0 [+init,+docs] (worker://plurnk/docs/**) <1,-1>",
592
- },
593
- ];
594
- for (const { statement, exemplar } of surveys) {
595
- await this.#dispatch({ statement, workspaceId, workerId, loopId, turnId, sequence: nextActionIndex, origin: "plurnk", onDispatch });
596
- nextActionIndex++;
597
- turnZeroMoves.push(exemplar);
598
- }
599
- }
600
- // {§worker-initialization-entry} — write the kernel's turn-0 initialization OPEN at sequence 1: PLAN → the FINDs actually
601
- // foisted above (real, their results already in the log) → SEND signal 102. Dynamic — it reflects
602
- // the true survey, never a frozen print — and OPEN: the worked example the model orients on,
603
- // so the grammar can stay thin.
604
- if (workerFirstLoop) {
605
- const initialization = [
606
- "# PLAN0\n* Initialization complete.\n* Next: address the prompt.",
607
- ...turnZeroMoves,
608
- "## SEND0 [102]\nNext, address the prompt.",
609
- ].join("\n\n");
610
- await this.#dispatcher.writeInitializationEntry({ verbatim: initialization, workerId, loopId, turnId, sequence: 1 });
611
- }
612
- }
613
- // {§methods-loop-run-open-paths}: selected workspace paths belong to
614
- // the prompt frame. Publish the frame, then dispatch ordinary core READs
615
- // in that same turn; missing/non-member paths retain their normal 4xx.
616
- for (const raw of turnOpenPaths) {
617
- const pathname = raw.startsWith("/") ? raw : `/${raw}`;
618
- const fileRead = {
619
- op: "READ", suffix: "", signal: null, lineMarker: null,
620
- target: {
621
- kind: "url", raw: `file://${pathname}`, scheme: "file",
622
- username: null, password: null, hostname: null, port: null,
623
- pathname, query: null, fragment: null,
624
- },
625
- body: null, position: UNKNOWN_POSITION,
626
- };
627
- await this.#dispatch({
628
- statement: fileRead, workspaceId, workerId, loopId, turnId,
629
- sequence: nextActionIndex, origin: "plurnk", onDispatch,
630
- });
631
- nextActionIndex++;
632
- }
633
- // {§env-delta-log-pull} — materialize ambient observations before packet
634
- // composition and reserve their action indices. {§exec-stream} owns the
635
- // distinct byte-cursor path for this worker's streams.
636
- // {§exec-poll} — EXEC `<0>` is turn-scoped: reap the worker's open turn-scoped streams (necessarily
637
- // from a prior turn — this runs before the turn's own spawns) so a `<0>` never survives into
638
- // the subsequent turn. The terminal output then surfaces born-OPEN via the stream-delta path.
639
- await this.#reapTurnScopedStreams(workerId);
640
- nextActionIndex += await this.#materializeEnvironmentDeltas({ workspaceId, workerId, loopId, turnId, fromSequence: nextActionIndex });
641
- nextActionIndex += await this.#materializeStreamDeltas({ workspaceId, workerId, loopId, turnId, fromSequence: nextActionIndex });
642
- // The post-reconciliation Git snapshot above is threaded into the packet
643
- // and every budget rebuild; overflow never shells again.
644
- // Notices are non-terminal observations, never operation-failure truth.
645
- // Drain once and thread the same set through every grinder rebuild.
646
- const notices = this.#notices.drain(loopId)
647
- .filter((event) => event.level !== "info");
648
- // Build the model request packet ({§packet-stored-shape}). The log build
649
- // queries log_entries scoped to the worker — the prompt entry just
650
- // written (if turn 1) is part of that query result.
651
- let promptProjection = "automatic";
652
- const buildPacket = () => this.#packets.buildRequestPacket({
653
- initialMessages: messages,
654
- requirements,
655
- workspaceId,
656
- workerId,
657
- loopId,
658
- currentTurnSeq: seq,
659
- provider,
660
- gitStatus,
661
- notices,
662
- transientOpenLogEntryId,
663
- promptProjection,
664
- });
665
- let requestPacket = await buildPacket();
666
- // SPEC {§grinder} — budget grinder, pre-LLM: reclaim window on actual overflow.
667
- const enforced = await this.#packets.enforceBudget({
668
- packet: requestPacket, provider, loopId, turnId,
669
- recordOverflow: async (pressure) => {
670
- await this.#problems.record({
671
- workerId,
672
- loopId,
673
- turnId,
674
- sequence: nextActionIndex++,
675
- origin: "plurnk",
676
- source: "engine",
677
- result: curationOverflowFailure(pressure),
678
- });
679
- },
680
- rebuild: buildPacket,
681
- });
682
- requestPacket = enforced.packet;
683
- let boundaryRolledBack = enforced.boundaryRolledBack;
684
- let modelMessages = PacketWire.packetToWireMessages(requestPacket);
685
- // Curation pressure and provider generation are independent. The
686
- // provider owns its configured total output envelope.
687
- let response;
688
- let splitResponse;
689
- let railGrammar;
690
- let railResponseGrammar;
691
- let railEvidence;
692
- let emissionAttempts = 0;
693
- let providerCallInFlight = false;
694
- let modelCallSequence = 0;
695
- let currentEmissionAttempt = 0;
696
- let providerAttemptId = null;
697
- let providerModelCall = null;
698
- let providerAttemptAttributions = [];
699
- const providerSignal = this.#loopSignal(loopId) ?? signal;
700
- // {§client-metadata}
701
- const { client } = await WorkspaceSettings.read(this.#db, workspaceId);
702
- const loopSeq = (await this.#db.engine_loop_sequence.get({ loop_id: loopId }))?.sequence ?? loopId;
703
- const providerIdentity = await this.#resolveWorkerProviderIdentity(workerId);
704
- const { workerId: providerWorkerId, primaryWorkerId } = providerIdentity;
705
- const classifyProviderAttempt = async (id, attemptSplit, emissionAttempt, accepted) => {
706
- const result = await this.#db.engine_classify_turn_attempt_response.run({
707
- id,
708
- accepted: accepted ? 1 : 0,
709
- parse_errors: JSON.stringify(attemptSplit.parseErrors),
710
- });
711
- if (result.changes !== 1) {
712
- throw new Error(`emission attempt ${id} was not awaiting classification`);
713
- }
714
- emissionAttempts = emissionAttempt;
715
- };
716
- const recoverCapacityPacket = async () => {
717
- let baselineMessages = modelMessages;
718
- if (promptProjection === "automatic") {
719
- promptProjection = "withheld";
720
- const candidate = await buildPacket();
721
- const candidateMessages = PacketWire.packetToWireMessages(candidate);
722
- if (JSON.stringify(candidateMessages) !== JSON.stringify(baselineMessages)) {
723
- requestPacket = candidate;
724
- modelMessages = candidateMessages;
725
- return true;
726
- }
727
- baselineMessages = candidateMessages;
728
- }
729
- if (!boundaryRolledBack) {
730
- await this.#packets.rollbackNewestBoundary(loopId, turnId);
731
- boundaryRolledBack = true;
732
- const candidate = await buildPacket();
733
- const candidateMessages = PacketWire.packetToWireMessages(candidate);
734
- if (JSON.stringify(candidateMessages) !== JSON.stringify(baselineMessages)) {
735
- requestPacket = candidate;
736
- modelMessages = candidateMessages;
737
- return true;
738
- }
739
- }
740
- return false;
741
- };
742
- try {
743
- // {§turn-lifecycle}: bracket the complete provider-attempt window with liveness notices.
744
- if (!signal?.aborted)
745
- this.#notices.push(workspaceId, loopId, { source: "engine:turn", kind: "turn_awaiting_model", level: "info", message: "awaiting model response" });
746
- const railConstraint = await this.#grammarConstraint(provider);
747
- railGrammar = railConstraint?.transport;
748
- railResponseGrammar = railConstraint?.response;
749
- const attemptLimit = readEmissionAttempts();
750
- const strikeStreak = this.#strikes.streak(loopId);
751
- for (let attempt = 1; attempt <= attemptLimit;) {
752
- // Capacity recovery may rebuild and resend the request without
753
- // consuming a grammar-emission attempt. Every logical provider
754
- // call still receives its own durable sequence and accounting.
755
- currentEmissionAttempt = attempt;
756
- modelCallSequence++;
757
- const attributionContext = Object.freeze({
758
- workspaceId: String(workspaceId),
759
- workerId: providerWorkerId,
760
- primaryWorkerId,
761
- loop: loopSeq,
762
- turn: seq,
763
- attempt: modelCallSequence,
764
- });
765
- providerAttemptAttributions = await this.#attemptAttributions(provider, attributionContext);
766
- requestPacket = { ...requestPacket, attributions: providerAttemptAttributions };
767
- providerModelCall = await ModelCall.open(this.#db, {
768
- turnId,
769
- sequence: modelCallSequence,
770
- kind: "emission",
771
- attributions: providerAttemptAttributions,
772
- model: provider.model,
773
- });
774
- const attemptRow = await this.#db.engine_open_turn_attempt.get({
775
- model_call_id: providerModelCall.id,
776
- });
777
- if (attemptRow === undefined) {
778
- throw new Error(`Engine.runTurn: provider call ${modelCallSequence} did not open`);
779
- }
780
- providerAttemptId = attemptRow.id;
781
- providerCallInFlight = true;
782
- const currentModelCall = providerModelCall;
783
- let completedResponse;
784
- try {
785
- completedResponse = await observed(// {§observability-boundary}
786
- "provider.generate", { model: provider.model, attempt: modelCallSequence }, async (span) => {
787
- try {
788
- const generated = await provider.generate({
789
- messages: modelMessages,
790
- workerId: providerWorkerId,
791
- primaryWorkerId,
792
- signal: providerSignal,
793
- grammar: railGrammar,
794
- strikes: strikeStreak,
795
- attributions: providerAttemptAttributions.length > 0
796
- ? providerAttemptAttributions
797
- : undefined,
798
- client: client ?? undefined,
799
- workspaceId: String(workspaceId),
800
- loop: loopSeq,
801
- turn: seq,
802
- observeRequest: currentModelCall.observeRequest,
803
- callKind: "emission",
804
- }); // {§provider-surface-generate} {§provider-guarantees-signal-wired} {§provider-guarantees-serial-attempts} {§attribution} {§client-metadata}
805
- currentModelCall.assertAccounting(generated.accounting);
806
- providerCallInFlight = false;
807
- recordCounter(PROVIDER_CALLS, {
808
- model: provider.model,
809
- attempt: modelCallSequence,
810
- status: "resolved",
811
- });
812
- span.setAttribute("status", "resolved");
813
- return generated;
814
- }
815
- catch (error) {
816
- if (error instanceof ProviderError) {
817
- currentModelCall.assertAccounting(error.accounting);
818
- }
819
- throw error;
820
- }
821
- });
822
- }
823
- catch (error) {
824
- if (!(error instanceof ProviderError)
825
- || error.kind !== "capacity_exceeded"
826
- || providerSignal?.aborted === true
827
- || !await recoverCapacityPacket()) {
828
- throw error;
829
- }
830
- const failure = _a.#providerFailure(error, providerSignal);
831
- await currentModelCall.fail(failure, error.capacity ?? null);
832
- providerCallInFlight = false;
833
- await this.#problems.record({
834
- workerId,
835
- loopId,
836
- turnId,
837
- sequence: nextActionIndex++,
838
- origin: "plurnk",
839
- source: "provider",
840
- result: failure,
841
- });
842
- // Include the durable recovery signal in the replacement
843
- // request while preserving the selected recovery posture.
844
- requestPacket = await buildPacket();
845
- modelMessages = PacketWire.packetToWireMessages(requestPacket);
846
- continue;
847
- }
848
- response = completedResponse;
849
- await currentModelCall.observeResponse(completedResponse);
850
- railEvidence = railGrammar === undefined
851
- ? undefined
852
- : _a.#requireGrammarEvidence(completedResponse);
853
- splitResponse = this.#splitResponse(completedResponse);
854
- await classifyProviderAttempt(attemptRow.id, splitResponse, attempt, splitResponse.emissionValid);
855
- if (splitResponse.emissionValid)
856
- break;
857
- attempt++;
858
- }
859
- if (!signal?.aborted)
860
- this.#notices.push(workspaceId, loopId, { source: "engine:turn", kind: "turn_generated", level: "info", message: "parsing model response" });
861
- }
862
- catch (err) {
863
- // This handler owns only provider-call failures. Parser, cost, SQL,
864
- // and engine-contract failures retain their original source.
865
- if (err instanceof ModelCallPersistenceError || err instanceof ProviderAccountingIntegrityError)
866
- throw err;
867
- if (!providerCallInFlight)
868
- throw err;
869
- providerCallInFlight = false;
870
- if (providerAttemptId === null || providerModelCall === null) {
871
- throw new Error("provider call failed without durable model-call and attempt identities", { cause: err });
872
- }
873
- const failure = _a.#providerFailure(err, providerSignal);
874
- const capacityFailure = err instanceof ProviderError && err.kind === "capacity_exceeded";
875
- // {§provider-interrupted-attempt} — a provider-declared interruption
876
- // carries response evidence without becoming a completed exchange.
877
- // Persist it as an unaccepted attempt before settling the failure.
878
- if (err instanceof ProviderError && err.attempt !== undefined) {
879
- response = err.attempt;
880
- await providerModelCall.observeResponse(response, failure);
881
- splitResponse = this.#splitResponse(response);
882
- await classifyProviderAttempt(providerAttemptId, splitResponse, currentEmissionAttempt, false);
883
- }
884
- else {
885
- await providerModelCall.fail(failure, err instanceof ProviderError ? err.capacity ?? null : null);
886
- if (!capacityFailure)
887
- emissionAttempts = currentEmissionAttempt;
888
- }
889
- // {§turn-never-blank} — a ProviderError means no completed exchange exists.
890
- // Persist its exact RFC 9457 result before propagating it. Grammar evidence
891
- // and its engine-owned verdict exist only on completed responses
892
- // ({§rail-truth-engine-verdict}). Cancellation is lifecycle truth, not a
893
- // provider failure. Close the
894
- // attempted turn without inventing an assistant response, then let
895
- // runLoop/Daemon settle the exact 504/499 loop result.
896
- if (providerSignal?.aborted) {
897
- await this.#db.engine_close_turn.run({
898
- id: turnId,
899
- status: providerSignal.reason === LOOP_TIMEOUT_REASON ? 504 : 499,
900
- packet: StoredPacket.stringify(requestPacket),
901
- usage_curation_budget: this.#packets.curationBudgetFor(provider),
902
- finish_reason: splitResponse?.callMetadata.finishReason ?? null,
903
- model: splitResponse?.callMetadata.model ?? provider.model,
904
- meta: JSON.stringify(response?.meta ?? {}),
905
- });
906
- throw err;
907
- }
908
- const recorded = await this.#problems.record({
909
- workerId,
910
- loopId,
911
- turnId,
912
- sequence: nextActionIndex,
913
- origin: "plurnk",
914
- source: "provider",
915
- result: failure,
916
- });
917
- // The provider call was attempted, but no completed exchange exists.
918
- // Persist the exact request half and failure status; omitting assistant
919
- // is materially different from fabricating an empty model turn.
920
- await this.#db.engine_close_turn.run({
921
- id: turnId,
922
- status: recorded.result.status,
923
- packet: StoredPacket.stringify(requestPacket),
924
- usage_curation_budget: this.#packets.curationBudgetFor(provider),
925
- finish_reason: splitResponse?.callMetadata.finishReason ?? null,
926
- model: splitResponse?.callMetadata.model ?? provider.model,
927
- meta: JSON.stringify(response?.meta ?? {}),
928
- });
929
- if (capacityFailure) {
930
- return {
931
- turnId,
932
- status: recorded.result.status,
933
- outcomes: [],
934
- fingerprint: "",
935
- capacityHardStop: true,
936
- capacityFailure: recorded.result,
937
- steerStruck: false,
938
- emissionAttempts,
939
- emissionExhausted: false,
940
- };
941
- }
942
- throw new OperationFailureError(recorded.result, { cause: err });
943
- }
944
- if (response === undefined || splitResponse === undefined || providerModelCall === null) {
945
- throw new Error("provider attempt loop completed without a response");
946
- }
947
- const emissionModelCallId = providerModelCall.id;
948
- if (!splitResponse.emissionValid) {
949
- // {§invalid-emission-attempts} The first consecutive exhaustion
950
- // publishes only the raw final response and generic recovery fact.
951
- let rejectedModelEntryId;
952
- if (allowInvalidEmissionRecovery) {
953
- rejectedModelEntryId = await this.#dispatcher.writeModelEntry({
954
- verbatim: splitResponse.packetAssistant.content,
955
- workerId,
956
- loopId,
957
- turnId,
958
- sequence: nextActionIndex,
959
- folded: true,
960
- modelCallId: emissionModelCallId,
961
- admission: "rejected",
962
- });
963
- this.#notices.push(workspaceId, loopId, {
964
- source: "engine:grammar",
965
- kind: "invalid_emission",
966
- level: "error",
967
- message: INVALID_EMISSION_RECOVERY_MESSAGE,
968
- });
969
- }
970
- await this.#db.engine_close_turn.run({
971
- id: turnId,
972
- status: allowInvalidEmissionRecovery ? TURN_STATUS_IMPLICIT_CONTINUE : 500,
973
- packet: StoredPacket.stringify(requestPacket),
974
- usage_curation_budget: this.#packets.curationBudgetFor(provider),
975
- finish_reason: splitResponse.callMetadata.finishReason,
976
- model: splitResponse.callMetadata.model,
977
- meta: JSON.stringify(response.meta ?? {}),
978
- });
979
- return {
980
- turnId,
981
- status: allowInvalidEmissionRecovery ? TURN_STATUS_IMPLICIT_CONTINUE : 500,
982
- outcomes: [],
983
- fingerprint: "",
984
- capacityHardStop: false,
985
- steerStruck: false,
986
- emissionAttempts,
987
- emissionExhausted: true,
988
- ...(rejectedModelEntryId === undefined ? {} : { rejectedModelEntryId }),
989
- };
990
- }
991
- // {§packet-stored-shape} — admitted emission data extends the packet;
992
- // provider-call metadata remains on the Turn row.
993
- const { packetAssistant, callMetadata, parseNotices, recoverableParseErrors, } = splitResponse; // raw assistant content is opaque — split, never interpreted — {§provider-guarantees-assistantraw-opaque}
994
- for (const notice of parseNotices) {
995
- this.#notices.push(workspaceId, loopId, notice);
996
- }
997
- // Non-fatal provider transport notices on an accepted turn. Forward each
998
- // Notice with a content-offset `line:col`;
999
- // the model resolves it against its own emission — READ the folded model-emission row at the
1000
- // cited lines ({§model-entry}) — not an embedded snippet that would duplicate the emission.
1001
- for (const notice of response.notices ?? []) {
1002
- const located = typeof notice.position === "number"
1003
- ? this.#offsetToLineColumn(packetAssistant.content, notice.position)
1004
- : null;
1005
- this.#notices.push(workspaceId, loopId, {
1006
- source: notice.source,
1007
- kind: notice.kind,
1008
- message: notice.message ?? "",
1009
- level: notice.level,
1010
- ...(located !== null
1011
- ? { position: { type: "content-offset", line: located.line, column: located.column } }
1012
- : {}),
1013
- });
1014
- }
1015
- // Grade configured local evidence independently. Endpoint-owned
1016
- // constraints remain provider observations. {§rail-truth-engine-verdict}
1017
- let railKeys;
1018
- if (railGrammar !== undefined) {
1019
- if (railEvidence === undefined)
1020
- throw new Error("configured GBNF response has no final grammar evidence");
1021
- if (railResponseGrammar === undefined)
1022
- throw new Error("configured GBNF has no response grammar");
1023
- let verdict = null;
1024
- try {
1025
- verdict = validateGbnf(railResponseGrammar, railEvidence.input);
1026
- }
1027
- catch (cause) {
1028
- _a.#warnRailVerdictGapOnce(cause.message);
1029
- }
1030
- railKeys = {
1031
- railsAttached: railEvidence.transported ? "client" : "withheld",
1032
- railsVerdict: verdict?.status ?? "unverifiable",
1033
- };
1034
- if (verdict !== null && verdict.status !== "accept") {
1035
- const contentPosition = verdict.pos >= railEvidence.contentStart
1036
- ? verdict.pos - railEvidence.contentStart
1037
- : null;
1038
- const located = contentPosition === null
1039
- ? null
1040
- : this.#offsetToLineColumn(packetAssistant.content, contentPosition);
1041
- this.#notices.push(workspaceId, loopId, {
1042
- source: "engine:rails",
1043
- kind: "grammar_unenforced",
1044
- message: verdict.status === "reject"
1045
- ? `emission rejects the grammar at raw code point ${verdict.pos}`
1046
- : `emission is an incomplete grammar sentence (ends at raw code point ${verdict.pos})`,
1047
- level: "warn",
1048
- ...(located === null
1049
- ? {}
1050
- : { position: { type: "content-offset", line: located.line, column: located.column } }),
1051
- });
1052
- }
1053
- }
1054
- const opsCount = packetAssistant.ops.length;
1055
- const finalOp = packetAssistant.ops.at(-1);
399
+ // {§turn-ops-admission-path} — source acquisition ends before this seam.
400
+ // Every admitted producer program is scheduled, dispatched, recorded, and
401
+ // completed here; inference is only the model-specific way one program is
402
+ // acquired and supplied with BARE capability.
403
+ async executeAdmittedTurn({ statements, source, sourceFolded, sourceModelCallId = null, sourceReasoningItems, origin, workspaceId, workerId, loopId, turnId, fromSequence, maxCommands = Number.POSITIVE_INFINITY, enforceIdle = false, failOnOperationError = false, recoverableParseErrors = [], bare, signal, onDispatch, }) {
404
+ const plan = statements[0];
405
+ const finalOp = statements.at(-1);
1056
406
  if (finalOp?.op !== "SEND") {
1057
- // Text emissions cannot reach this point without a disposition;
1058
- // this fail-hard guard also keeps Mock's trusted pre-parsed seam
1059
- // from creating runtime states that production admission forbids.
1060
- throw new Error("an admitted emission must end in a disposition SEND");
407
+ throw new Error("an admitted operation batch must end in a disposition SEND");
408
+ }
409
+ if (source !== null && plan?.op !== "PLAN") {
410
+ throw new Error("an admitted source-backed turnOps program must begin with PLAN");
1061
411
  }
1062
412
  const dispositionSignal = finalOp.signal;
1063
413
  if (typeof dispositionSignal !== "number" || !TERMINAL_SEND_SIGNALS.has(dispositionSignal)) {
1064
- throw new Error("an admitted emission must end in a disposition SEND");
414
+ throw new Error("an admitted turnOps program must end in a terminal disposition SEND");
1065
415
  }
1066
416
  const sendOp = finalOp;
1067
- // {§send} the terminal contract — engine error states verify a terminal claim against loop
1068
- // state, never trusting the model's code. They strike via turn.steerStruck
1069
- // ({§engine-rails}): the loop continues, the model sees the steering hint not the strike
1070
- // count, and a non-resolver spins out to the engine's 500.
417
+ let turnStatus = dispositionSignal;
1071
418
  let steerStruck = false;
1072
- // Engine errors raised this turn, minted as op='error' log rows after dispatch (they share the
1073
- // post-dispatch sequence counter). {§operation-result-uniform-error-channel}
1074
419
  const pendingEngineErrors = [];
1075
- // Terminal adjudication moved to the DISPATCHER ({§send-premature-terminate}, the unified
1076
- // pending set): the terminal SEND is judged AT ITS OWN DISPATCH — after the emission's
1077
- // earlier ops executed — so a same-turn KILL+[200] repairs in one turn and a same-turn
1078
- // WORK+[200] is caught. A refused final disposition (409) strikes via
1079
- // the dispatch-loop check below.
1080
- let turnStatus = dispositionSignal;
1081
- // Idle turn: an implicit-continue (102) that did no WORK — its ops are only PLAN/SEND, no mid op.
1082
- // The model continued with nothing to do.
1083
- const midOpsCount = packetAssistant.ops.filter((op) => op.op !== "PLAN" && op.op !== "SEND").length
420
+ const middleCount = statements.filter((statement) => statement.op !== "PLAN" && statement.op !== "SEND").length
1084
421
  + recoverableParseErrors.length;
1085
- if (!steerStruck && turnStatus === TURN_STATUS_IMPLICIT_CONTINUE && midOpsCount === 0) {
422
+ if (enforceIdle && turnStatus === TURN_STATUS_IMPLICIT_CONTINUE && middleCount === 0) {
1086
423
  steerStruck = true;
1087
424
  pendingEngineErrors.push("idle_turn");
1088
425
  }
1089
- // Close the turn with the final packet, status, and usage stats.
1090
- const packet = StoredPacket.admit(requestPacket, packetAssistant, response.assistantRaw);
1091
- await this.#db.engine_close_turn.run({
1092
- id: turnId,
1093
- status: turnStatus,
1094
- packet: StoredPacket.stringify(packet),
1095
- usage_curation_budget: this.#packets.curationBudgetFor(provider), // {§tokenomics-client-gauge}
1096
- finish_reason: callMetadata.finishReason,
1097
- model: callMetadata.model,
1098
- // Opaque provider metadata plus engine-authored rail keys.
1099
- // {§meta-passthrough}, {§rail-truth-engine-verdict}
1100
- meta: JSON.stringify({ ...(response.meta ?? {}), ...(railKeys ?? {}) }),
1101
- });
1102
- // Dispatch model ops starting at nextActionIndex (continues the
1103
- // turn's running counter after any pre-model writes).
1104
- //
1105
- // Max-commands ceiling: OFF by default (unlimited) — every generated op dispatches.
1106
- // A degenerate op-loop is a sampler failure guarded at generation, not by dropping
1107
- // already-generated work post-hoc. The ceiling is an OPT-IN operator/client bound:
1108
- // when set, overflow ops drop without per-op log entries (no forensics flood) and the
1109
- // model gets one notices signal next packet.
1110
- // {§operator-config-workspace-max-commands} — workspace maxCommands min()s the env ceiling.
1111
- const maxCommands = Math.min(readMaxCommands(), (await WorkspaceSettings.read(this.#db, workspaceId)).maxCommands ?? Number.POSITIVE_INFINITY);
1112
- // PLAN (intended goals) and the final disposition SEND are not actions —
1113
- // they always dispatch and never count against the cap. maxCommands
1114
- // bounds real actions only; maxCommands:0 still admits a plan and a disposition
1115
- // (the PLAN/SEND ops, zero actions), which is its only coherent meaning.
1116
426
  let realCommands = 0;
1117
- const admittedOps = packetAssistant.ops.filter((op) => {
1118
- return op.op === "PLAN"
1119
- || op === sendOp
1120
- || realCommands++ < maxCommands;
1121
- });
1122
- const opsToDispatch = scheduleTurnOps(admittedOps);
1123
- await this.#dispatcher.prepareEditBatches(opsToDispatch.filter((statement) => statement.op === "EDIT"), {
1124
- workspaceId, workerId, loopId, turnId,
1125
- origin, onDispatch,
1126
- });
1127
- const droppedCount = opsCount - opsToDispatch.length;
1128
- const bareStatements = opsToDispatch.filter((statement) => statement.op === "BARE");
427
+ const admitted = statements.filter((statement) => statement.op === "PLAN"
428
+ || statement === sendOp
429
+ || realCommands++ < maxCommands);
430
+ const scheduled = scheduleTurnOps(admitted.flatMap(expandSafeUriTargetGroup));
431
+ await this.#dispatcher.prepareEditBatches(scheduled.filter((statement) => statement.op === "EDIT"), { workspaceId, workerId, loopId, turnId, origin, onDispatch });
432
+ const droppedCount = statements.length - admitted.length;
433
+ const bareStatements = scheduled.filter((statement) => statement.op === "BARE");
1129
434
  let bareResults = null;
1130
435
  const outcomes = [];
1131
- // Running counter — a multi-file READ writes N rows from one statement (rowsWritten),
1132
- // so the next op's sequence picks up after them. Collapses to nextActionIndex+i when
1133
- // every op writes one row (the common case).
1134
- let rowSeq = nextActionIndex;
436
+ let rowSequence = fromSequence;
1135
437
  let parseErrorsRecorded = false;
1136
438
  const recordRecoverableParseErrors = async () => {
1137
439
  if (parseErrorsRecorded)
@@ -1142,8 +444,8 @@ class TurnRunner {
1142
444
  workerId,
1143
445
  loopId,
1144
446
  turnId,
1145
- sequence: rowSeq++,
1146
- origin: "model",
447
+ sequence: rowSequence++,
448
+ origin,
1147
449
  source: "grammar",
1148
450
  result: Results.failure("grammar:parser", "invalid-operation-syntax", 400, error.message, {}, {
1149
451
  line: error.line,
@@ -1158,39 +460,35 @@ class TurnRunner {
1158
460
  onDispatch?.(recorded.id);
1159
461
  }
1160
462
  };
1161
- for (const statement of opsToDispatch) {
1162
- if (statement.op === "SEND"
1163
- && typeof statement.signal === "number"
1164
- && TERMINAL_SEND_SIGNALS.has(statement.signal)) {
463
+ for (const statement of scheduled) {
464
+ if (statement === sendOp) {
1165
465
  await recordRecoverableParseErrors();
1166
- // {§worker-optimistic-settlement} — SEND judges the refreshed
1167
- // lifecycle after this turn's own fast streams receive one
1168
- // bounded opportunity to conclude. This is not a sleep and
1169
- // does not delay a later turn for older monitored streams.
1170
466
  const execHandler = this.#schemes.get("exec");
1171
- await execHandler?.settleTurnSpawns?.(workerId, turnId, readOptimisticSettlementMs(), providerSignal);
467
+ await execHandler?.settleTurnSpawns?.(workerId, turnId, readOptimisticSettlementMs(), signal);
1172
468
  }
1173
- const result = await observed(// {§observability-boundary}
1174
- "op.dispatch", { op: statement.op }, async (span) => {
469
+ const result = await observed("op.dispatch", { op: statement.op }, async (span) => {
1175
470
  let dispatchResult;
1176
471
  if (statement.op === "BARE") {
472
+ if (bare === undefined) {
473
+ throw new Error(`${origin} turnOps cannot execute BARE without provider acquisition context`);
474
+ }
1177
475
  if (bareResults === null) {
1178
476
  const batch = await this.#runBareBatch({
1179
477
  statements: bareStatements,
1180
- provider: childProvider,
478
+ provider: bare.provider,
1181
479
  turnId,
1182
- modelCallSequenceStart: modelCallSequence + 1,
480
+ modelCallSequenceStart: bare.modelCallSequenceStart,
1183
481
  workspaceId,
1184
482
  workerId,
1185
- primaryWorkerId,
1186
- loopSequence: loopSeq,
1187
- turnSequence: seq,
1188
- signal: providerSignal,
483
+ primaryWorkerId: bare.primaryWorkerId,
484
+ loopSequence: bare.loopSequence,
485
+ turnSequence: bare.turnSequence,
486
+ signal: bare.signal,
1189
487
  });
1190
488
  bareResults = new Map(batch.map((item) => [item.statement, item]));
1191
489
  }
1192
- const bare = bareResults.get(statement);
1193
- if (bare === undefined) {
490
+ const bareResult = bareResults.get(statement);
491
+ if (bareResult === undefined) {
1194
492
  throw new Error("BARE statement reached dispatch without its batch result");
1195
493
  }
1196
494
  dispatchResult = await this.#dispatcher.recordBareResult({
@@ -1199,16 +497,21 @@ class TurnRunner {
1199
497
  workerId,
1200
498
  loopId,
1201
499
  turnId,
1202
- sequence: rowSeq,
500
+ sequence: rowSequence,
1203
501
  origin,
1204
502
  onDispatch,
1205
- }, bare.result, bare.modelCallId);
503
+ }, bareResult.result, bareResult.modelCallId);
1206
504
  }
1207
505
  else {
1208
506
  dispatchResult = await this.#dispatcher.dispatch({
1209
- statement, workspaceId, workerId, loopId, turnId,
1210
- sequence: rowSeq,
1211
- origin, onDispatch,
507
+ statement,
508
+ workspaceId,
509
+ workerId,
510
+ loopId,
511
+ turnId,
512
+ sequence: rowSequence,
513
+ origin,
514
+ onDispatch,
1212
515
  });
1213
516
  }
1214
517
  span.setAttribute("status", dispatchResult.status);
@@ -1216,6 +519,8 @@ class TurnRunner {
1216
519
  return dispatchResult;
1217
520
  });
1218
521
  outcomes.push({ op: statement.op, status: result.status });
522
+ if (failOnOperationError && result.status >= 400)
523
+ throw new OperationFailureError(result);
1219
524
  for (const normalization of result.scopeNormalizations ?? []) {
1220
525
  this.#notices.push(workspaceId, loopId, {
1221
526
  source: "engine:slicer",
@@ -1224,39 +529,20 @@ class TurnRunner {
1224
529
  message: `Scope <${normalization.requested.join(",")}> was normalized to <${normalization.canonical.join(",")}>.`,
1225
530
  });
1226
531
  }
1227
- // {§engine-rails} — a refused final disposition leaves both loop
1228
- // and turn continuing, and its 409 steering ruling strikes once.
1229
532
  if (statement === sendOp && result.status === 409) {
1230
533
  steerStruck = true;
1231
534
  turnStatus = TURN_STATUS_IMPLICIT_CONTINUE;
1232
- await this.#db.engine_reconcile_turn_status.run({ id: turnId, status: turnStatus });
1233
- }
1234
- // {§send-300-choices}: a question resolves through the proposal system; whatever the
1235
- // resolution (answer/reject/timeout), the LOOP continues to the turn where the model
1236
- // reads it; the turn record is a continue, never a 300 terminal.
1237
- if (statement === sendOp && sendOp.signal === 300 && result.status !== 409) {
1238
- turnStatus = TURN_STATUS_IMPLICIT_CONTINUE;
1239
- await this.#db.engine_reconcile_turn_status.run({ id: turnId, status: turnStatus });
1240
535
  }
1241
- // A broadcast [202] is a conditional wait, not an unconditional turn status:
1242
- // live work parks at 202; completed-but-unobserved work continues at 102; an
1243
- // empty join completes at 200. Persist and return the dispatcher's actual ruling.
1244
536
  if (statement === sendOp
1245
537
  && result.status !== 409
1246
538
  && sendOp.target === null
1247
539
  && sendOp.signal === 202
1248
540
  && result.status !== 202) {
1249
541
  turnStatus = result.status;
1250
- await this.#db.engine_reconcile_turn_status.run({ id: turnId, status: turnStatus });
1251
542
  }
1252
- rowSeq += result.rowsWritten ?? 1;
543
+ rowSequence += result.rowsWritten ?? 1;
1253
544
  }
1254
545
  await recordRecoverableParseErrors();
1255
- // Engine rail failures mint as op='error' log rows at the turn's next
1256
- // free sequence. Bounded syntax failures were recorded in their
1257
- // authored turn before its terminal disposition.
1258
- let errSeq = rowSeq;
1259
- // max_commands_exceeded IS model-facing: dropped ops the model emitted that didn't run.
1260
546
  if (droppedCount > 0)
1261
547
  pendingEngineErrors.push("max_commands_exceeded");
1262
548
  for (const kind of pendingEngineErrors) {
@@ -1278,47 +564,950 @@ class TurnRunner {
1278
564
  workerId,
1279
565
  loopId,
1280
566
  turnId,
1281
- sequence: errSeq++,
1282
- origin: "plurnk",
567
+ sequence: rowSequence++,
568
+ origin: "_plurnk",
1283
569
  source: "rail",
1284
570
  result: Results.failure("engine:rail", problem.code, problem.status, problem.detail, {}, extensions),
1285
571
  });
1286
572
  }
1287
- // {§log-row-self-explains} — model-operation failures remain on their
1288
- // own rows; genuine engine faults fail hard and mint no substitute row.
1289
- // {§model-entry} — mirror this turn's verbatim emission back as a `model` row, so the NEXT
1290
- // packet shows the model exactly what it last produced. ALWAYS born FOLDED — the old
1291
- // born-OPEN-on-error auto-trigger was conditional helpfulness that bred its own hazards
1292
- // (a 24k-char ramble mirrored open re-injects itself into the next packet: cost,
1293
- // contamination, pressure feedback).
1294
- // {§encrypted-reasoning-carrier} — core relays every provider-normalized
1295
- // item unchanged.
1296
- const reasoningItems = response.assistant.reasoningEncrypted?.length
1297
- ? response.assistant.reasoningEncrypted
1298
- : undefined;
1299
- if (packetAssistant.content.trim().length > 0 || reasoningItems !== undefined) {
1300
- await this.#dispatcher.writeModelEntry({
1301
- verbatim: packetAssistant.content,
573
+ if (source !== null) {
574
+ await this.#dispatcher.writeTurnOps({
575
+ verbatim: source,
1302
576
  workerId,
1303
577
  loopId,
1304
578
  turnId,
1305
- sequence: errSeq++,
1306
- folded: true,
1307
- modelCallId: emissionModelCallId,
1308
- ...(reasoningItems !== undefined ? { reasoningItems } : {}),
579
+ sequence: rowSequence,
580
+ origin,
581
+ folded: sourceFolded,
582
+ modelCallId: sourceModelCallId,
583
+ ...(sourceReasoningItems !== undefined ? { reasoningItems: sourceReasoningItems } : {}),
1309
584
  });
1310
585
  }
586
+ await Turn.complete(this.#db, turnId, turnStatus);
1311
587
  return {
1312
- turnId,
1313
588
  status: turnStatus,
1314
589
  outcomes,
1315
- fingerprint: StrikeRail.fingerprintTurn(packetAssistant.ops),
1316
- capacityHardStop: false,
590
+ fingerprint: StrikeRail.fingerprintTurn(statements),
1317
591
  steerStruck,
1318
- emissionAttempts,
1319
- emissionExhausted: false,
1320
592
  };
1321
593
  }
594
+ async runTurn({ provider, childProvider = provider, messages, requirements = "", workspaceId, workerId, loopId, signal, onDispatch, turnNumber = 1, maxTurns = 50, invalidEmissionRecoveryEntryId, }) {
595
+ const allowInvalidEmissionRecovery = invalidEmissionRecoveryEntryId === null;
596
+ const transientOpenLogEntryId = typeof invalidEmissionRecoveryEntryId === "number"
597
+ ? invalidEmissionRecoveryEntryId
598
+ : null;
599
+ // === Producer-neutral turn container ===
600
+ //
601
+ // Every producer opens the same durable turn and completes it only
602
+ // after its ordered operations settle. Model packets and provider
603
+ // metadata are optional inference evidence, not turn identity.
604
+ const initialSequence = await this.#db.engine_next_turn_sequence.get({ loop_id: loopId });
605
+ if (initialSequence === undefined)
606
+ throw new Error("Engine.runTurn: next turn sequence is unavailable");
607
+ // Turn-0 foists that belong to the Worker (catalog preview, AGENTS) gate
608
+ // on its first inference history, not on loop sequence: honest client or
609
+ // runtime administrative turns may precede the first provider exchange.
610
+ // Per-loop foists such as
611
+ // {§prompt-entry} still fires once per loop. Durable publication state,
612
+ // rather than the model-turn ordinal, prevents an overflow diversion
613
+ // from replaying the prompt and its automatic path READs.
614
+ const loopRow = await this.#db.engine_get_loop_prompt.get({ loop_id: loopId });
615
+ const priorInference = await this.#db.engine_worker_has_inference_history.get({
616
+ worker_id: workerId,
617
+ });
618
+ const workerFirstInference = priorInference?.present === 0;
619
+ const createdTurnIds = [];
620
+ try {
621
+ // {§worker-initialization-entry} — turn zero is an ordinary `_plurnk`
622
+ // operation turn. Its model-facing zero is a phase label; durable turn
623
+ // coordinates remain one-based.
624
+ const initializationTurn = initialSequence.next === 1 && workerFirstInference
625
+ ? await Turn.open(this.#db, { loopId, producer: "_plurnk", kind: "initialization" })
626
+ : null;
627
+ if (initializationTurn !== null)
628
+ createdTurnIds.push(initializationTurn.id);
629
+ let modelTurn = initializationTurn === null
630
+ ? await Turn.open(this.#db, { loopId, producer: "model", kind: "inference" })
631
+ : null;
632
+ if (modelTurn !== null)
633
+ createdTurnIds.push(modelTurn.id);
634
+ // {§env-delta-log-pull} — creation atomically owns the ordinary worker
635
+ // baseline or fork snapshot. Packet assembly consumes, never invents,
636
+ // that durable observation boundary.
637
+ const ambientCursor = await this.#db.engine_initialize_ambient_cursor.get({ workspace_id: workspaceId, worker_id: workerId });
638
+ if (ambientCursor?.ambient_event_cursor === null || ambientCursor === undefined) {
639
+ throw new Error(`worker ${workerId} has no durable ambient observation boundary`);
640
+ }
641
+ // Threaded per turn, never engine state, so concurrent loops on
642
+ // different providers each read their own honest tokenizer values.
643
+ const systemContext = (contextTurnId) => ({
644
+ db: this.#db, workspaceId, workerId, functionalityWorkerId: workerId, loopId, turnId: contextTurnId,
645
+ writer: "_plurnk",
646
+ signal: this.#loopSignal(loopId),
647
+ streamEventNotify: this.#streamEventNotify,
648
+ wakeWorkerNotify: this.#wakeWorkerNotify,
649
+ weigh: this.#weighContent,
650
+ mimetypes: this.#mimetypes,
651
+ defaultChannelFor: (s) => this.#schemes.defaultChannelFor(s, workerId),
652
+ pushNotice: (notice) => this.#notices.push(workspaceId, loopId, notice),
653
+ requestInteraction: (request) => this.#interactions.request(request, { workspaceId, workerId, loopId, turnId: contextTurnId }, this.#loopSignal(loopId)),
654
+ });
655
+ let systemCtx = systemContext(initializationTurn?.id ?? modelTurn.id);
656
+ const initializationStatements = [];
657
+ // {§worker-initialization-entry} — the worker's first turn is the worked
658
+ // example itself: an ordinary PLAN, the actual orienting operations,
659
+ // and an ordinary terminal SEND.
660
+ if (initializationTurn !== null) {
661
+ const plan = {
662
+ op: "PLAN", delimiter: "", annotation: null,
663
+ signal: null, target: null, lineMarker: null,
664
+ body: [{
665
+ content: "Discover the tooling available and survey the workspace file root.",
666
+ priority: "medium",
667
+ status: "in_progress",
668
+ }],
669
+ position: UNKNOWN_POSITION,
670
+ };
671
+ initializationStatements.push(plan);
672
+ // {§turn0-agents-stunt} — the project AGENTS.md (materialized by LoopDocs as
673
+ // worker://~/_plurnk/agents.md) gets one foisted READ on the worker's first
674
+ // loop, so local repo guidance is visible turn-0 content. Global policy
675
+ // stays in the system prompt; nothing else is force-read.
676
+ if (workerFirstInference) {
677
+ const agentsEntry = await this.#db.crud_find_workspace_entry.get({
678
+ workspace_id: workspaceId,
679
+ owner_id: workerId,
680
+ scheme: "worker",
681
+ authority: "",
682
+ pathname: generatedPathname("/agents.md"),
683
+ });
684
+ if (agentsEntry !== undefined) {
685
+ const agentsTarget = {
686
+ kind: "url", raw: "worker://~/_plurnk/agents.md", scheme: "worker",
687
+ username: null, password: null, hostname: "~", port: null,
688
+ pathname: generatedPathname("/agents.md"), query: null, fragment: null,
689
+ };
690
+ const agentsRead = {
691
+ op: "READ", delimiter: "", annotation: null, signal: ["+_plurnk", "+init", "+policy"], target: agentsTarget,
692
+ lineMarker: null, body: null, position: UNKNOWN_POSITION,
693
+ };
694
+ initializationStatements.push(agentsRead);
695
+ }
696
+ }
697
+ }
698
+ // The persistent search-index pass (_search-index.maintain) attaches
699
+ // every readable entry/log projection to complete graph/FTS/vector derivations.
700
+ // NOT an action: no log entry, no sequence slot,
701
+ // not dispatched. There is no materialized manifest entry — the catalog
702
+ // is served on demand by FIND: recursive when asked, shallow-mapped below.
703
+ // {§semantic-embed-dedup} — one pass-wide semantic plan binds every
704
+ // chunk counter to the derivation identity it produces.
705
+ // SPEC {§membership} D4/D5 — git-ls-files workspace membership, resolved at
706
+ // prompt-composition (EMI is eager + exhaustive — git is the only bound). When the
707
+ // workspace's project_root is a git working tree, tracked files are
708
+ // members without a client `add`; active members are materialized
709
+ // (disk → body channel) so they appear in the catalog. No-ops
710
+ // on headless / non-git workspaces. Runs BEFORE the derivation pump so
711
+ // this turn's packet reflects them.
712
+ // Joining here is the correctness boundary: passive workspace creation and
713
+ // attachment spend nothing, while the model never runs against partial
714
+ // graph/vector coverage.
715
+ await this.#warmWorkspace(systemCtx, false);
716
+ // The warm materialized membership before deriving. This second pass is the
717
+ // ordinary cheap change detector and captures any drift that landed meanwhile.
718
+ const fsDivergences = await GitMembership.indexGitMembership(systemCtx);
719
+ // {§packet-git-status} — one post-reconciliation snapshot supplies both
720
+ // the compact packet summary and each causal file event's exact XY state.
721
+ const gitStatus = await GitState.status(this.#db, workspaceId, this.#loopSignal(loopId));
722
+ await this.#logFsFictions(workspaceId, fsDivergences, gitStatus);
723
+ // The refresh above may have changed bodies (including model/client edits
724
+ // since the startup warm). Re-derive to completion before packet/model
725
+ // construction. Membership is already current, so this pass does not
726
+ // consume the filesystem divergences a second time.
727
+ await this.#warmWorkspace(systemCtx, true, false);
728
+ // Turn-0 catalog preview (PLURNK_SERVICE_FILES_ITEMS, {§actor-boundary-catalog-preview}):
729
+ // Seven bodyless FIND surveys in the worker's packetless initialization turn establish the skills tree
730
+ // (authored and Plurnk-generated families), attached tools tree, enabled agents, project, commons, and
731
+ // private surfaces in that order.
732
+ // Their `init` classification lets the model curate this opening survey as one log set.
733
+ if (initializationTurn !== null) {
734
+ // {§operator-config-workspace-files-items} — workspace filesItems replaces the env default.
735
+ const { filesItems: workspaceMI } = await WorkspaceSettings.read(this.#db, workspaceId);
736
+ const filesItems = workspaceMI !== null ? normalizeFilesItems(workspaceMI) : readFilesItems();
737
+ if (filesItems !== null && workerFirstInference) { // {§actor-boundary-catalog-preview} — once per worker
738
+ const catalogSchemes = await this.#db.engine_scheme_catalog_summary.all({ workspace_id: workspaceId });
739
+ const fileItems = catalogSchemes.find(({ scheme }) => scheme === "file")?.shallow_items ?? 0;
740
+ const fileCap = filesItems > 0 && fileItems > 0 ? Math.min(filesItems, fileItems) : null;
741
+ // {§tools-resource-materialization} — PLURNK_MCP_EXPANDED servers
742
+ // contribute one complete family-document READ each, directly
743
+ // after the family survey, so turn 0 names every tool they
744
+ // expose in order. (The family document is the whole tool
745
+ // teaching — per-target child documents do not exist.)
746
+ const registry = this.#executors();
747
+ const toolExpansions = [];
748
+ for (const tag of registry?.availableRuntimes(workerId) ?? []) {
749
+ const entry = registry?.entry(tag, workerId);
750
+ if (entry?.resourcesPath !== "/tools" || entry.expandTools !== true)
751
+ continue;
752
+ toolExpansions.push({
753
+ statement: {
754
+ op: "READ", delimiter: "", annotation: `enabled tools: ${tag}`, signal: ["+_plurnk", "+init", "+tools"],
755
+ target: { kind: "url", raw: `worker://~/_plurnk/tools/${tag}.md`, scheme: "worker", username: null, password: null, hostname: "~", port: null, pathname: generatedPathname(`/tools/${tag}.md`), query: null, fragment: null },
756
+ body: null, lineMarker: { marks: [1, -1] }, position: UNKNOWN_POSITION,
757
+ },
758
+ });
759
+ }
760
+ const surveys = [
761
+ {
762
+ statement: {
763
+ op: "FIND", delimiter: "", annotation: null, signal: ["+_plurnk", "+init", "+skills"],
764
+ target: { kind: "url", raw: "worker://~/_plurnk/skills/*.md", scheme: "worker", username: null, password: null, hostname: "~", port: null, pathname: generatedPathname("/skills/*.md"), query: null, fragment: null },
765
+ body: null, lineMarker: { marks: [1, -1] }, position: UNKNOWN_POSITION,
766
+ },
767
+ },
768
+ {
769
+ statement: {
770
+ op: "FIND", delimiter: "", annotation: null, signal: ["+_plurnk", "+init", "+skills"],
771
+ target: { kind: "url", raw: "worker://~/_plurnk/skills/plurnk/*.md", scheme: "worker", username: null, password: null, hostname: "~", port: null, pathname: generatedPathname("/skills/plurnk/*.md"), query: null, fragment: null },
772
+ body: null, lineMarker: { marks: [1, -1] }, position: UNKNOWN_POSITION,
773
+ },
774
+ },
775
+ {
776
+ // {§tools-resource-materialization} — enabled tool families
777
+ // (MCP servers) survey at family level; each row's summary is
778
+ // the server one-liner or its flagship invocation form, so the
779
+ // discovery row itself orients. Servers named in
780
+ // PLURNK_MCP_EXPANDED add a second survey of their complete
781
+ // tool tree.
782
+ statement: {
783
+ op: "FIND", delimiter: "", annotation: "enabled tools", signal: ["+_plurnk", "+init", "+tools"],
784
+ target: { kind: "url", raw: "worker://~/_plurnk/tools/*.md", scheme: "worker", username: null, password: null, hostname: "~", port: null, pathname: generatedPathname("/tools/*.md"), query: null, fragment: null },
785
+ body: null, lineMarker: { marks: [1, -1] }, position: UNKNOWN_POSITION,
786
+ },
787
+ },
788
+ ...toolExpansions,
789
+ {
790
+ // {§a2a-agents-catalog} — enabled outbound agents survey at
791
+ // alias level; each row's summary is the agent's identity line,
792
+ // the exact card stays pullable through READ a2a://<alias>.
793
+ statement: {
794
+ op: "FIND", delimiter: "", annotation: "enabled agents", signal: ["+_plurnk", "+init", "+agents"],
795
+ target: { kind: "url", raw: "worker://~/_plurnk/agents/*.md", scheme: "worker", username: null, password: null, hostname: "~", port: null, pathname: generatedPathname("/agents/*.md"), query: null, fragment: null },
796
+ body: null, lineMarker: { marks: [1, -1] }, position: UNKNOWN_POSITION,
797
+ },
798
+ },
799
+ {
800
+ statement: {
801
+ op: "FIND", delimiter: "", annotation: "workspace files", signal: ["+_plurnk", "+init"],
802
+ target: { kind: "local", raw: "*" },
803
+ body: null,
804
+ lineMarker: fileCap === null ? null : { marks: [1, fileCap] },
805
+ position: UNKNOWN_POSITION,
806
+ },
807
+ },
808
+ {
809
+ statement: {
810
+ op: "FIND", delimiter: "", annotation: "workspace entries", signal: ["+_plurnk", "+init"],
811
+ target: { kind: "url", raw: "worker:///*", scheme: "worker", username: null, password: null, hostname: null, port: null, pathname: "/*", query: null, fragment: null },
812
+ body: null, lineMarker: null, position: UNKNOWN_POSITION,
813
+ },
814
+ },
815
+ {
816
+ statement: {
817
+ op: "FIND", delimiter: "", annotation: "worker entries", signal: ["+_plurnk", "+init"],
818
+ target: { kind: "url", raw: "worker://~/*", scheme: "worker", username: null, password: null, hostname: "~", port: null, pathname: "/*", query: null, fragment: null },
819
+ body: null, lineMarker: null, position: UNKNOWN_POSITION,
820
+ },
821
+ },
822
+ ];
823
+ initializationStatements.push(...surveys.map(({ statement }) => statement));
824
+ }
825
+ const send = {
826
+ op: "SEND", delimiter: "", annotation: null,
827
+ signal: 102, target: null, lineMarker: null,
828
+ body: { raw: "Next: Address the prompt.", json: null },
829
+ position: UNKNOWN_POSITION,
830
+ };
831
+ initializationStatements.push(send);
832
+ const source = TurnOps.renderInternal(initializationStatements);
833
+ const admitted = TurnOps.parseInternal(source);
834
+ const result = await this.executeAdmittedTurn({
835
+ statements: admitted,
836
+ source,
837
+ sourceFolded: false,
838
+ origin: "_plurnk",
839
+ workspaceId,
840
+ workerId,
841
+ loopId,
842
+ turnId: initializationTurn.id,
843
+ fromSequence: 1,
844
+ failOnOperationError: true,
845
+ signal: this.#loopSignal(loopId),
846
+ onDispatch,
847
+ });
848
+ if (result.status !== TURN_STATUS_IMPLICIT_CONTINUE) {
849
+ throw new Error(`initialization SEND returned ${result.status}; expected ${TURN_STATUS_IMPLICIT_CONTINUE}`);
850
+ }
851
+ }
852
+ // Initialization is a complete preceding turn, not a set of rows
853
+ // interleaved with the model boundary. The engine may now acquire the
854
+ // inference turn inside the same warmed workspace cycle.
855
+ if (modelTurn === null) {
856
+ modelTurn = await Turn.open(this.#db, { loopId, producer: "model", kind: "inference" });
857
+ createdTurnIds.push(modelTurn.id);
858
+ }
859
+ const seq = modelTurn.sequence;
860
+ const turnId = modelTurn.id;
861
+ systemCtx = systemContext(turnId);
862
+ // Pre-model writes. Each prompt the model has not seen yet becomes a
863
+ // `prompt` operation row whose target is its durable prompt:// entry.
864
+ // Model operations continue the same turn sequence after these rows.
865
+ let nextActionIndex = 1;
866
+ const turnOpenPaths = [];
867
+ if (turnNumber === 1 && loopRow?.prompt_published === 0) {
868
+ const promptRow = loopRow; // {§prompt-entry} — one durable publication per loop
869
+ if (promptRow !== undefined && typeof promptRow.prompt === "string" && promptRow.prompt.length > 0) {
870
+ const openPaths = assertOpenPaths(JSON.parse(promptRow.open_paths), `Loop ${loopId} open_paths`);
871
+ const promptLoopSeq = promptRow.sequence;
872
+ const promptPath = promptTarget(promptLoopSeq, 1);
873
+ const entry = {
874
+ channels: { body: { content: promptRow.prompt, mimetype: "text/markdown" } },
875
+ attributes: {
876
+ openPaths,
877
+ ...(promptRow.prompt_source === null ? {} : { source: promptRow.prompt_source }),
878
+ },
879
+ };
880
+ await EntryCrud.writeEntry({ authority: "", pathname: promptPath.pathname }, entry, systemCtx, "prompt", workerId);
881
+ turnOpenPaths.push(...openPaths);
882
+ const promptLogId = await this.#writePromptLog({
883
+ workerId,
884
+ loopId,
885
+ turnId,
886
+ sequence: nextActionIndex++,
887
+ target: promptPath,
888
+ content: promptRow.prompt,
889
+ source: promptRow.prompt_source,
890
+ });
891
+ onDispatch?.(promptLogId);
892
+ }
893
+ }
894
+ // {§prompt-loop-containment}: the loop contains every prompt that arrived
895
+ // while it ran. Publish each undelivered frame oldest-first exactly once.
896
+ {
897
+ const loopSeqRow = await this.#db.engine_loop_sequence.get({ loop_id: loopId });
898
+ const loopSeq = loopSeqRow?.sequence ?? loopId;
899
+ const prefix = promptLoopPrefix(loopSeq);
900
+ const undelivered = (await this.#db.drain_undelivered_prompts_for_loop.all({
901
+ owner_id: workerId,
902
+ pattern: `${prefix}%`,
903
+ prefix_len: prefix.length,
904
+ loop_id: loopId,
905
+ }))
906
+ .filter((row) => typeof row.content === "string" && row.content.length > 0);
907
+ for (const injectedRow of undelivered) {
908
+ const attributes = parsePromptAttributes(injectedRow.attributes, `Prompt ${injectedRow.pathname} attributes`);
909
+ if (attributes.openPaths !== undefined) {
910
+ turnOpenPaths.push(...assertOpenPaths(attributes.openPaths, `Prompt ${injectedRow.pathname} openPaths`));
911
+ }
912
+ const ordinal = Number(injectedRow.pathname.split("/").filter(Boolean).at(-1));
913
+ const promptLogId = await this.#writePromptLog({
914
+ workerId,
915
+ loopId,
916
+ turnId,
917
+ sequence: nextActionIndex++,
918
+ target: promptTarget(loopSeq, ordinal),
919
+ content: injectedRow.content,
920
+ source: promptSourceFromAttributes(attributes, `Prompt ${injectedRow.pathname} attributes`),
921
+ });
922
+ onDispatch?.(promptLogId);
923
+ }
924
+ }
925
+ // {§methods-loop-run-open-paths}: selected workspace paths belong to
926
+ // the prompt frame. Publish the frame, then dispatch ordinary core READs
927
+ // in that same turn; missing/non-member paths retain their normal 4xx.
928
+ for (const raw of turnOpenPaths) {
929
+ const pathname = raw.startsWith("/") ? raw : `/${raw}`;
930
+ const fileRead = {
931
+ op: "READ", delimiter: "", annotation: null, signal: null, lineMarker: null,
932
+ target: {
933
+ kind: "url", raw: `file://${pathname}`, scheme: "file",
934
+ username: null, password: null, hostname: null, port: null,
935
+ pathname, query: null, fragment: null,
936
+ },
937
+ body: null, position: UNKNOWN_POSITION,
938
+ };
939
+ await this.#dispatch({
940
+ statement: fileRead, workspaceId, workerId, loopId, turnId,
941
+ sequence: nextActionIndex, origin: "_plurnk", onDispatch,
942
+ });
943
+ nextActionIndex++;
944
+ }
945
+ // {§env-delta-log-pull} — materialize ambient observations before packet
946
+ // composition and reserve their action indices. {§exec-stream} owns the
947
+ // distinct byte-cursor path for this worker's streams.
948
+ // {§exec-poll} — EXEC `<0>` is turn-scoped: reap the worker's open turn-scoped streams (necessarily
949
+ // from a prior turn — this runs before the turn's own spawns) so a `<0>` never survives into
950
+ // the subsequent turn. The terminal output then surfaces born-OPEN via the stream-delta path.
951
+ await this.#reapTurnScopedStreams(workerId);
952
+ nextActionIndex += await this.#materializeEnvironmentDeltas({ workspaceId, workerId, loopId, turnId, fromSequence: nextActionIndex });
953
+ nextActionIndex += await this.#materializeStreamDeltas({ workerId, loopId, turnId, fromSequence: nextActionIndex });
954
+ // The post-reconciliation Git snapshot above is threaded into the packet
955
+ // and every budget rebuild; overflow never shells again.
956
+ // Notices are non-terminal observations, never operation-failure truth.
957
+ // Drain once and thread the same set through every overflow rebuild.
958
+ const notices = this.#notices.drain(loopId)
959
+ .filter((event) => event.level !== "info");
960
+ // Build the model request packet ({§packet-stored-shape}). The log build
961
+ // queries log_entries scoped to the worker — the prompt entry just
962
+ // written (if turn 1) is part of that query result.
963
+ let promptProjection = "automatic";
964
+ const buildPacket = () => this.#packets.buildRequestPacket({
965
+ initialMessages: messages,
966
+ requirements,
967
+ workspaceId,
968
+ workerId,
969
+ loopId,
970
+ currentTurnSeq: seq,
971
+ provider,
972
+ gitStatus,
973
+ notices,
974
+ transientOpenLogEntryId,
975
+ promptProjection,
976
+ });
977
+ let requestPacket = await buildPacket();
978
+ // {§overflow-turn} — measured overflow diverts this would-be model
979
+ // turn into an ordinary packetless `_plurnk` operation batch. Every
980
+ // visibility effect goes through Dispatcher FOLD; provider I/O is
981
+ // structurally unreachable on this branch.
982
+ const pressure = this.#packets.curationOverflow(requestPacket, provider);
983
+ if (pressure !== null) {
984
+ const folds = await OverflowTurn.plan(this.#db, loopId, turnId);
985
+ await Turn.becomeOverflow(this.#db, turnId);
986
+ const internalStatements = [
987
+ OverflowTurn.planStatement(),
988
+ ...folds.map(({ statement }) => statement),
989
+ OverflowTurn.sendStatement(),
990
+ ];
991
+ const source = TurnOps.renderInternal(internalStatements);
992
+ const admitted = TurnOps.parseInternal(source);
993
+ const executed = await this.executeAdmittedTurn({
994
+ statements: admitted,
995
+ source,
996
+ sourceFolded: true,
997
+ origin: "_plurnk",
998
+ workspaceId,
999
+ workerId,
1000
+ loopId,
1001
+ turnId,
1002
+ fromSequence: nextActionIndex,
1003
+ failOnOperationError: true,
1004
+ signal: this.#loopSignal(loopId),
1005
+ onDispatch,
1006
+ });
1007
+ if (executed.status !== TURN_STATUS_IMPLICIT_CONTINUE) {
1008
+ throw new Error(`overflow SEND returned ${executed.status}; expected ${TURN_STATUS_IMPLICIT_CONTINUE}`);
1009
+ }
1010
+ const rebuilt = await buildPacket();
1011
+ const remaining = this.#packets.curationOverflow(rebuilt, provider);
1012
+ const curationFailure = folds.length === 0 || remaining !== null
1013
+ ? curationOverflowFailure(remaining ?? pressure)
1014
+ : undefined;
1015
+ return {
1016
+ createdTurnIds,
1017
+ turnId,
1018
+ producer: "_plurnk",
1019
+ kind: "overflow",
1020
+ status: curationFailure?.status ?? TURN_STATUS_IMPLICIT_CONTINUE,
1021
+ outcomes: [],
1022
+ fingerprint: "",
1023
+ capacityHardStop: false,
1024
+ steerStruck: false,
1025
+ emissionAttempts: 0,
1026
+ emissionExhausted: false,
1027
+ ...(curationFailure === undefined ? {} : { curationFailure }),
1028
+ };
1029
+ }
1030
+ let modelMessages = PacketWire.packetToWireMessages(requestPacket);
1031
+ // Curation pressure and provider generation are independent. The
1032
+ // provider owns its configured total output envelope.
1033
+ let response;
1034
+ let splitResponse;
1035
+ let railGrammar;
1036
+ let railResponseGrammar;
1037
+ let railAllowWithheld = false;
1038
+ let railEvidence;
1039
+ let emissionAttempts = 0;
1040
+ let providerCallInFlight = false;
1041
+ let modelCallSequence = 0;
1042
+ let currentEmissionAttempt = 0;
1043
+ let providerAttemptId = null;
1044
+ let providerModelCall = null;
1045
+ let providerAttemptAttributions = [];
1046
+ const providerSignal = this.#loopSignal(loopId) ?? signal;
1047
+ // {§client-metadata}
1048
+ const { client } = await WorkspaceSettings.read(this.#db, workspaceId);
1049
+ const loopSeq = (await this.#db.engine_loop_sequence.get({ loop_id: loopId }))?.sequence ?? loopId;
1050
+ const providerIdentity = await this.#resolveWorkerProviderIdentity(workerId);
1051
+ const { workerId: providerWorkerId, primaryWorkerId } = providerIdentity;
1052
+ const classifyProviderAttempt = async (id, attemptSplit, emissionAttempt, accepted) => {
1053
+ const result = await this.#db.engine_classify_turn_attempt_response.run({
1054
+ id,
1055
+ accepted: accepted ? 1 : 0,
1056
+ parse_errors: JSON.stringify(attemptSplit.parseErrors),
1057
+ });
1058
+ if (result.changes !== 1) {
1059
+ throw new Error(`emission attempt ${id} was not awaiting classification`);
1060
+ }
1061
+ emissionAttempts = emissionAttempt;
1062
+ };
1063
+ const recoverCapacityPacket = async () => {
1064
+ let baselineMessages = modelMessages;
1065
+ if (promptProjection === "automatic") {
1066
+ promptProjection = "withheld";
1067
+ const candidate = await buildPacket();
1068
+ const candidateMessages = PacketWire.packetToWireMessages(candidate);
1069
+ if (JSON.stringify(candidateMessages) !== JSON.stringify(baselineMessages)) {
1070
+ requestPacket = candidate;
1071
+ modelMessages = candidateMessages;
1072
+ return true;
1073
+ }
1074
+ baselineMessages = candidateMessages;
1075
+ }
1076
+ return false;
1077
+ };
1078
+ try {
1079
+ // {§turn-lifecycle}: bracket the complete provider-attempt window with liveness notices.
1080
+ if (!signal?.aborted)
1081
+ this.#notices.push(workspaceId, loopId, { source: "engine:turn", kind: "turn_awaiting_model", level: "info", message: "awaiting model response" });
1082
+ const railConstraint = await this.#grammarConstraint(provider);
1083
+ railGrammar = railConstraint?.transport;
1084
+ railResponseGrammar = railConstraint?.response;
1085
+ railAllowWithheld = railConstraint?.allowWithheld ?? false;
1086
+ const attemptLimit = readEmissionAttempts();
1087
+ const strikeStreak = this.#strikes.streak(loopId);
1088
+ for (let attempt = 1; attempt <= attemptLimit;) {
1089
+ // Capacity recovery may rebuild and resend the request without
1090
+ // consuming a grammar-emission attempt. Every logical provider
1091
+ // call still receives its own durable sequence and accounting.
1092
+ currentEmissionAttempt = attempt;
1093
+ modelCallSequence++;
1094
+ const attributionContext = Object.freeze({
1095
+ workspaceId: String(workspaceId),
1096
+ workerId: providerWorkerId,
1097
+ primaryWorkerId,
1098
+ loop: loopSeq,
1099
+ turn: seq,
1100
+ attempt: modelCallSequence,
1101
+ });
1102
+ providerAttemptAttributions = await this.#attemptAttributions(provider, attributionContext);
1103
+ requestPacket = { ...requestPacket, attributions: providerAttemptAttributions };
1104
+ providerModelCall = await ModelCall.open(this.#db, {
1105
+ turnId,
1106
+ sequence: modelCallSequence,
1107
+ kind: "emission",
1108
+ attributions: providerAttemptAttributions,
1109
+ model: provider.model,
1110
+ });
1111
+ const attemptRow = await this.#db.engine_open_turn_attempt.get({
1112
+ model_call_id: providerModelCall.id,
1113
+ });
1114
+ if (attemptRow === undefined) {
1115
+ throw new Error(`Engine.runTurn: provider call ${modelCallSequence} did not open`);
1116
+ }
1117
+ providerAttemptId = attemptRow.id;
1118
+ providerCallInFlight = true;
1119
+ const currentModelCall = providerModelCall;
1120
+ let reasoningStarted = false;
1121
+ // {§notifications-reasoning-event} Only the parent emission is
1122
+ // conversational; BARE has no observer on its isolated calls.
1123
+ const observeReasoning = this.#reasoningEventNotify === undefined
1124
+ ? undefined
1125
+ : (delta) => {
1126
+ if (!reasoningStarted) {
1127
+ reasoningStarted = true;
1128
+ this.#reasoningEventNotify(workspaceId, {
1129
+ workerId,
1130
+ loopId,
1131
+ turnId,
1132
+ modelCallId: currentModelCall.id,
1133
+ phase: "start",
1134
+ });
1135
+ }
1136
+ this.#reasoningEventNotify(workspaceId, {
1137
+ workerId,
1138
+ loopId,
1139
+ turnId,
1140
+ modelCallId: currentModelCall.id,
1141
+ phase: "content",
1142
+ delta,
1143
+ });
1144
+ };
1145
+ let completedResponse;
1146
+ try {
1147
+ completedResponse = await observed(// {§observability-boundary}
1148
+ GEN_AI_REQUEST_SPAN, { model: provider.model, attempt: modelCallSequence }, async (span) => {
1149
+ try {
1150
+ const generated = await provider.generate({
1151
+ messages: modelMessages,
1152
+ workerId: providerWorkerId,
1153
+ primaryWorkerId,
1154
+ signal: providerSignal,
1155
+ grammar: railGrammar,
1156
+ strikes: strikeStreak,
1157
+ attributions: providerAttemptAttributions.length > 0
1158
+ ? providerAttemptAttributions
1159
+ : undefined,
1160
+ client: client ?? undefined,
1161
+ workspaceId: String(workspaceId),
1162
+ loop: loopSeq,
1163
+ turn: seq,
1164
+ observeRequest: currentModelCall.observeRequest,
1165
+ observeReasoning,
1166
+ callKind: "emission",
1167
+ }); // {§provider-surface-generate} {§provider-guarantees-signal-wired} {§provider-guarantees-serial-attempts} {§attribution} {§client-metadata}
1168
+ currentModelCall.assertAccounting(generated.accounting);
1169
+ providerCallInFlight = false;
1170
+ recordCounter(PROVIDER_CALLS, {
1171
+ model: provider.model,
1172
+ attempt: modelCallSequence,
1173
+ status: "resolved",
1174
+ });
1175
+ span.setAttribute("status", "resolved");
1176
+ settleGenAiResponse(span, generated);
1177
+ return generated;
1178
+ }
1179
+ catch (error) {
1180
+ if (error instanceof ProviderError) {
1181
+ currentModelCall.assertAccounting(error.accounting);
1182
+ }
1183
+ throw error;
1184
+ }
1185
+ }, genAiRequestOptions(ProviderInstantiate.aliasOf(provider) ?? "plurnk", provider.model));
1186
+ }
1187
+ catch (error) {
1188
+ if (!(error instanceof ProviderError)
1189
+ || error.kind !== "capacity_exceeded"
1190
+ || providerSignal?.aborted === true
1191
+ || !await recoverCapacityPacket()) {
1192
+ throw error;
1193
+ }
1194
+ const failure = _a.#providerFailure(error, providerSignal);
1195
+ await currentModelCall.fail(failure, error.capacity ?? null);
1196
+ providerCallInFlight = false;
1197
+ await this.#problems.record({
1198
+ workerId,
1199
+ loopId,
1200
+ turnId,
1201
+ sequence: nextActionIndex++,
1202
+ origin: "_plurnk",
1203
+ source: "provider",
1204
+ result: failure,
1205
+ });
1206
+ // Include the durable recovery signal in the replacement
1207
+ // request while preserving the selected recovery posture.
1208
+ requestPacket = await buildPacket();
1209
+ modelMessages = PacketWire.packetToWireMessages(requestPacket);
1210
+ continue;
1211
+ }
1212
+ finally {
1213
+ if (reasoningStarted) {
1214
+ this.#reasoningEventNotify(workspaceId, {
1215
+ workerId,
1216
+ loopId,
1217
+ turnId,
1218
+ modelCallId: currentModelCall.id,
1219
+ phase: "end",
1220
+ });
1221
+ }
1222
+ }
1223
+ response = completedResponse;
1224
+ await currentModelCall.observeResponse(completedResponse);
1225
+ railEvidence = railGrammar === undefined
1226
+ ? undefined
1227
+ : _a.#requireGrammarEvidence(completedResponse, railAllowWithheld);
1228
+ splitResponse = this.#splitResponse(completedResponse);
1229
+ await classifyProviderAttempt(attemptRow.id, splitResponse, attempt, splitResponse.emissionValid);
1230
+ if (splitResponse.emissionValid)
1231
+ break;
1232
+ attempt++;
1233
+ }
1234
+ if (!signal?.aborted)
1235
+ this.#notices.push(workspaceId, loopId, { source: "engine:turn", kind: "turn_generated", level: "info", message: "parsing model response" });
1236
+ }
1237
+ catch (err) {
1238
+ // This handler owns only provider-call failures. Parser, cost, SQL,
1239
+ // and engine-contract failures retain their original source.
1240
+ if (err instanceof ModelCallPersistenceError || err instanceof ProviderAccountingIntegrityError)
1241
+ throw err;
1242
+ if (!providerCallInFlight)
1243
+ throw err;
1244
+ providerCallInFlight = false;
1245
+ if (providerAttemptId === null || providerModelCall === null) {
1246
+ throw new Error("provider call failed without durable model-call and attempt identities", { cause: err });
1247
+ }
1248
+ const failure = _a.#providerFailure(err, providerSignal);
1249
+ const capacityFailure = err instanceof ProviderError && err.kind === "capacity_exceeded";
1250
+ // {§provider-interrupted-attempt} — a provider-declared interruption
1251
+ // carries response evidence without becoming a completed exchange.
1252
+ // Persist it as an unaccepted attempt before settling the failure.
1253
+ if (err instanceof ProviderError && err.attempt !== undefined) {
1254
+ response = err.attempt;
1255
+ await providerModelCall.observeResponse(response, failure);
1256
+ splitResponse = this.#splitResponse(response);
1257
+ await classifyProviderAttempt(providerAttemptId, splitResponse, currentEmissionAttempt, false);
1258
+ }
1259
+ else {
1260
+ await providerModelCall.fail(failure, err instanceof ProviderError ? err.capacity ?? null : null);
1261
+ if (!capacityFailure)
1262
+ emissionAttempts = currentEmissionAttempt;
1263
+ }
1264
+ // {§turn-never-blank} — a ProviderError means no completed exchange exists.
1265
+ // Persist its exact RFC 9457 result before propagating it. Grammar evidence
1266
+ // and its engine-owned verdict exist only on completed responses
1267
+ // ({§rail-truth-engine-verdict}). Cancellation is lifecycle truth, not a
1268
+ // provider failure. Close the
1269
+ // attempted turn without inventing an assistant response, then let
1270
+ // runLoop/Daemon settle the exact 504/499 loop result.
1271
+ if (providerSignal?.aborted) {
1272
+ const status = providerSignal.reason === LOOP_TIMEOUT_REASON ? 504 : 499;
1273
+ await Turn.recordInference(this.#db, turnId, {
1274
+ packet: StoredPacket.stringify(requestPacket),
1275
+ usageCurationBudget: this.#packets.curationBudgetFor(provider),
1276
+ finishReason: splitResponse?.callMetadata.finishReason ?? null,
1277
+ model: splitResponse?.callMetadata.model ?? provider.model,
1278
+ meta: JSON.stringify(response?.meta ?? {}),
1279
+ });
1280
+ await Turn.complete(this.#db, turnId, status);
1281
+ throw err;
1282
+ }
1283
+ const recorded = await this.#problems.record({
1284
+ workerId,
1285
+ loopId,
1286
+ turnId,
1287
+ sequence: nextActionIndex,
1288
+ origin: "_plurnk",
1289
+ source: "provider",
1290
+ result: failure,
1291
+ });
1292
+ // The provider call was attempted, but no completed exchange exists.
1293
+ // Persist the exact request half and failure status; omitting assistant
1294
+ // is materially different from fabricating an empty model turn.
1295
+ await Turn.recordInference(this.#db, turnId, {
1296
+ packet: StoredPacket.stringify(requestPacket),
1297
+ usageCurationBudget: this.#packets.curationBudgetFor(provider),
1298
+ finishReason: splitResponse?.callMetadata.finishReason ?? null,
1299
+ model: splitResponse?.callMetadata.model ?? provider.model,
1300
+ meta: JSON.stringify(response?.meta ?? {}),
1301
+ });
1302
+ await Turn.complete(this.#db, turnId, recorded.result.status);
1303
+ if (capacityFailure) {
1304
+ return {
1305
+ createdTurnIds,
1306
+ turnId,
1307
+ producer: "model",
1308
+ kind: "inference",
1309
+ status: recorded.result.status,
1310
+ outcomes: [],
1311
+ fingerprint: "",
1312
+ capacityHardStop: true,
1313
+ capacityFailure: recorded.result,
1314
+ steerStruck: false,
1315
+ emissionAttempts,
1316
+ emissionExhausted: false,
1317
+ };
1318
+ }
1319
+ throw new OperationFailureError(recorded.result, { cause: err });
1320
+ }
1321
+ if (response === undefined || splitResponse === undefined || providerModelCall === null) {
1322
+ throw new Error("provider attempt loop completed without a response");
1323
+ }
1324
+ const emissionModelCallId = providerModelCall.id;
1325
+ if (!splitResponse.emissionValid) {
1326
+ // {§invalid-emission-attempts} The first consecutive exhaustion
1327
+ // publishes only the raw final response and generic recovery fact.
1328
+ let rejectedModelEntryId;
1329
+ if (allowInvalidEmissionRecovery) {
1330
+ rejectedModelEntryId = await this.#dispatcher.writeEmissionAttempt({
1331
+ verbatim: splitResponse.packetAssistant.content,
1332
+ workerId,
1333
+ loopId,
1334
+ turnId,
1335
+ sequence: nextActionIndex,
1336
+ modelCallId: emissionModelCallId,
1337
+ ...(response.assistant.reasoningEncrypted?.length
1338
+ ? { reasoningItems: response.assistant.reasoningEncrypted }
1339
+ : {}),
1340
+ });
1341
+ this.#notices.push(workspaceId, loopId, {
1342
+ source: "engine:grammar",
1343
+ kind: "invalid_emission",
1344
+ level: "error",
1345
+ message: INVALID_EMISSION_RECOVERY_MESSAGE,
1346
+ });
1347
+ }
1348
+ const status = allowInvalidEmissionRecovery ? TURN_STATUS_IMPLICIT_CONTINUE : 500;
1349
+ await Turn.recordInference(this.#db, turnId, {
1350
+ packet: StoredPacket.stringify(requestPacket),
1351
+ usageCurationBudget: this.#packets.curationBudgetFor(provider),
1352
+ finishReason: splitResponse.callMetadata.finishReason,
1353
+ model: splitResponse.callMetadata.model,
1354
+ meta: JSON.stringify(response.meta ?? {}),
1355
+ });
1356
+ await Turn.complete(this.#db, turnId, status);
1357
+ return {
1358
+ createdTurnIds,
1359
+ turnId,
1360
+ producer: "model",
1361
+ kind: "inference",
1362
+ status: allowInvalidEmissionRecovery ? TURN_STATUS_IMPLICIT_CONTINUE : 500,
1363
+ outcomes: [],
1364
+ fingerprint: "",
1365
+ capacityHardStop: false,
1366
+ steerStruck: false,
1367
+ emissionAttempts,
1368
+ emissionExhausted: true,
1369
+ ...(rejectedModelEntryId === undefined ? {} : { rejectedModelEntryId }),
1370
+ };
1371
+ }
1372
+ // {§packet-stored-shape} — admitted emission data extends the packet;
1373
+ // provider-call metadata remains on the Turn row.
1374
+ const { packetAssistant, callMetadata, parseNotices, recoverableParseErrors, } = splitResponse; // raw assistant content is opaque — split, never interpreted — {§provider-guarantees-assistantraw-opaque}
1375
+ for (const notice of parseNotices) {
1376
+ this.#notices.push(workspaceId, loopId, notice);
1377
+ }
1378
+ // Non-fatal provider transport notices on an accepted turn. Forward each
1379
+ // Notice with a content-offset `line:col`;
1380
+ // the model resolves it against its own emission — READ the folded turnOps row at the
1381
+ // cited lines ({§turn-ops-entry}) — not an embedded snippet that would duplicate the emission.
1382
+ for (const notice of response.notices ?? []) {
1383
+ const located = typeof notice.position === "number"
1384
+ ? this.#offsetToLineColumn(packetAssistant.content, notice.position)
1385
+ : null;
1386
+ this.#notices.push(workspaceId, loopId, {
1387
+ source: notice.source,
1388
+ kind: notice.kind,
1389
+ message: notice.message ?? "",
1390
+ level: notice.level,
1391
+ ...(located !== null
1392
+ ? { position: { type: "content-offset", line: located.line, column: located.column } }
1393
+ : {}),
1394
+ });
1395
+ }
1396
+ // Grade configured local evidence independently. Endpoint-owned
1397
+ // constraints remain provider observations. {§rail-truth-engine-verdict}
1398
+ let railKeys;
1399
+ if (railGrammar !== undefined) {
1400
+ if (railEvidence === undefined)
1401
+ throw new Error("configured GBNF response has no final grammar evidence");
1402
+ if (railResponseGrammar === undefined)
1403
+ throw new Error("configured GBNF has no response grammar");
1404
+ let verdict = null;
1405
+ try {
1406
+ verdict = validateGbnf(railResponseGrammar, railEvidence.input);
1407
+ }
1408
+ catch (cause) {
1409
+ _a.#warnRailVerdictGapOnce(cause.message);
1410
+ }
1411
+ railKeys = {
1412
+ railsAttached: railEvidence.transported ? "client" : "withheld",
1413
+ railsVerdict: verdict?.status ?? "unverifiable",
1414
+ };
1415
+ if (verdict !== null && verdict.status !== "accept") {
1416
+ const contentPosition = verdict.pos >= railEvidence.contentStart
1417
+ ? verdict.pos - railEvidence.contentStart
1418
+ : null;
1419
+ const located = contentPosition === null
1420
+ ? null
1421
+ : this.#offsetToLineColumn(packetAssistant.content, contentPosition);
1422
+ this.#notices.push(workspaceId, loopId, {
1423
+ source: "engine:rails",
1424
+ kind: "grammar_unenforced",
1425
+ message: verdict.status === "reject"
1426
+ ? `emission rejects the grammar at raw code point ${verdict.pos}`
1427
+ : `emission is an incomplete grammar sentence (ends at raw code point ${verdict.pos})`,
1428
+ level: "warn",
1429
+ ...(located === null
1430
+ ? {}
1431
+ : { position: { type: "content-offset", line: located.line, column: located.column } }),
1432
+ });
1433
+ }
1434
+ }
1435
+ // Attach the admitted inference evidence. The turn remains open until
1436
+ // the producer-neutral admitted-turn executor settles every operation
1437
+ // and its exact source artifact.
1438
+ const packet = StoredPacket.admit(requestPacket, packetAssistant, response.assistantRaw);
1439
+ await Turn.recordInference(this.#db, turnId, {
1440
+ packet: StoredPacket.stringify(packet),
1441
+ usageCurationBudget: this.#packets.curationBudgetFor(provider), // {§tokenomics-client-gauge}
1442
+ finishReason: callMetadata.finishReason,
1443
+ model: callMetadata.model,
1444
+ // Opaque provider metadata plus engine-authored rail keys.
1445
+ // {§meta-passthrough}, {§rail-truth-engine-verdict}
1446
+ meta: JSON.stringify({ ...(response.meta ?? {}), ...(railKeys ?? {}) }),
1447
+ });
1448
+ // {§operator-config-workspace-max-commands} — workspace maxCommands
1449
+ // narrows the operator ceiling before the admitted program reaches the
1450
+ // shared executor.
1451
+ const maxCommands = Math.min(readMaxCommands(), (await WorkspaceSettings.read(this.#db, workspaceId)).maxCommands ?? Number.POSITIVE_INFINITY);
1452
+ const reasoningItems = response.assistant.reasoningEncrypted?.length
1453
+ ? response.assistant.reasoningEncrypted
1454
+ : undefined;
1455
+ const executed = await this.executeAdmittedTurn({
1456
+ statements: packetAssistant.ops,
1457
+ source: splitResponse.sourceBacked ? packetAssistant.content : null,
1458
+ sourceFolded: true,
1459
+ sourceModelCallId: emissionModelCallId,
1460
+ ...(reasoningItems !== undefined ? { sourceReasoningItems: reasoningItems } : {}),
1461
+ origin: "model",
1462
+ workspaceId,
1463
+ workerId,
1464
+ loopId,
1465
+ turnId,
1466
+ fromSequence: nextActionIndex,
1467
+ maxCommands,
1468
+ enforceIdle: true,
1469
+ recoverableParseErrors,
1470
+ bare: {
1471
+ provider: childProvider,
1472
+ modelCallSequenceStart: modelCallSequence + 1,
1473
+ primaryWorkerId,
1474
+ loopSequence: loopSeq,
1475
+ turnSequence: seq,
1476
+ signal: providerSignal,
1477
+ },
1478
+ signal: providerSignal,
1479
+ onDispatch,
1480
+ });
1481
+ return {
1482
+ createdTurnIds,
1483
+ turnId,
1484
+ producer: "model",
1485
+ kind: "inference",
1486
+ status: executed.status,
1487
+ outcomes: executed.outcomes,
1488
+ fingerprint: executed.fingerprint,
1489
+ capacityHardStop: false,
1490
+ steerStruck: executed.steerStruck,
1491
+ emissionAttempts,
1492
+ emissionExhausted: false,
1493
+ };
1494
+ }
1495
+ catch (cause) {
1496
+ const completionFailures = [];
1497
+ for (const createdTurnId of createdTurnIds) {
1498
+ try {
1499
+ await Turn.failOpen(this.#db, createdTurnId);
1500
+ }
1501
+ catch (completionCause) {
1502
+ completionFailures.push(completionCause);
1503
+ }
1504
+ }
1505
+ if (completionFailures.length > 0) {
1506
+ throw new AggregateError([cause, ...completionFailures], "turn execution failed and its open turn containers could not all be completed");
1507
+ }
1508
+ throw cause;
1509
+ }
1510
+ }
1322
1511
  // Split the wire-level ProviderResponse into the two destinations:
1323
1512
  // packet.assistant gets the model's emission (content, ops, reasoning);
1324
1513
  // Turn columns get accepted-call metadata (finishReason, model). Physical
@@ -1412,6 +1601,7 @@ class TurnRunner {
1412
1601
  const reasoning = assistant.reasoning ?? null;
1413
1602
  return {
1414
1603
  packetAssistant: { content: assistant.content, ops, reasoning },
1604
+ sourceBacked: preParsedOps === undefined,
1415
1605
  callMetadata: { finishReason: assistant.finishReason, model: assistant.model },
1416
1606
  parseErrors,
1417
1607
  recoverableParseErrors: emissionValid ? recoverableParseErrors : [],
@@ -1424,8 +1614,8 @@ class TurnRunner {
1424
1614
  emissionValid,
1425
1615
  };
1426
1616
  }
1427
- // #note12 — the plugin-provided reference docs (schemes' + execs' `documentation`),
1428
- // materialized at worker://plurnk/docs/<name>.md by LoopDocs (like operator docs).
1617
+ // #note12 — plugin reference docs are materialized beneath
1618
+ // worker://~/_plurnk/skills/plurnk/ by LoopDocs.
1429
1619
  async #materializeEnvironmentDeltas(args) {
1430
1620
  const { workspaceId, workerId, loopId, turnId, fromSequence } = args;
1431
1621
  const rows = await this.#db.engine_pull_ambient_events.all({ workspace_id: workspaceId, worker_id: workerId });
@@ -1435,11 +1625,10 @@ class TurnRunner {
1435
1625
  let written = 0;
1436
1626
  for (const r of rows) {
1437
1627
  if (r.event_id === null || r.producer_worker_id === null || r.producer_worker_name === null || r.kind === null
1438
- || r.op === null || r.status_rx === null)
1628
+ || r.at === null || r.op === null || r.delimiter === null || r.tx === null || r.mimetype_tx === null
1629
+ || r.rx === null || r.mimetype_rx === null || r.status_rx === null || r.state === null)
1439
1630
  continue;
1440
1631
  const termination = r.kind === "loop_termination";
1441
- if (r.rx === null)
1442
- throw new Error(`ambient event ${r.event_id} has no materializable result`);
1443
1632
  const terminal = termination
1444
1633
  ? TerminalResult.parse(r.rx, `ambient loop-termination event ${r.event_id}`)
1445
1634
  : null;
@@ -1470,24 +1659,39 @@ class TurnRunner {
1470
1659
  }
1471
1660
  const inserted = await this.#db.engine_insert_ambient_delta.get({
1472
1661
  worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence: fromSequence + written,
1662
+ at: r.at,
1473
1663
  event_id: r.event_id,
1474
- source: r.source ?? WorkerControlAddress.render(r.producer_worker_name),
1664
+ source: WorkerControlAddress.render(r.producer_worker_name),
1475
1665
  op: r.op,
1666
+ delimiter: r.delimiter,
1667
+ signal: r.signal,
1476
1668
  scheme: r.scheme,
1669
+ username: r.username,
1670
+ password: r.password,
1477
1671
  hostname: r.hostname,
1672
+ port: r.port,
1478
1673
  pathname: r.pathname,
1674
+ query: r.query,
1675
+ fragment: r.fragment,
1676
+ line_marker: r.line_marker,
1677
+ tx: r.tx,
1678
+ mimetype_tx: r.mimetype_tx,
1479
1679
  rx: r.rx,
1480
- mimetype_rx: "application/json",
1680
+ mimetype_rx: r.mimetype_rx,
1481
1681
  status: r.status_rx,
1482
1682
  weight: LogBody.weight({
1483
1683
  op: r.op,
1484
1684
  attrs,
1485
- tx: "",
1685
+ tx: r.tx,
1486
1686
  rx: r.rx,
1487
- mimetypeTx: "text/plain",
1488
- mimetypeRx: "application/json",
1687
+ mimetypeTx: r.mimetype_tx,
1688
+ mimetypeRx: r.mimetype_rx,
1489
1689
  }, this.#weighContent),
1490
- expanded: terminal !== null && terminal.status >= 200 && terminal.status < 300 ? 1 : 0,
1690
+ state: r.state,
1691
+ outcome: r.outcome,
1692
+ folded: LogVisibility.serialize(terminal !== null && terminal.status >= 200 && terminal.status < 300
1693
+ ? LogVisibility.OPEN
1694
+ : LogVisibility.FOLDED),
1491
1695
  attrs,
1492
1696
  });
1493
1697
  const materialized = inserted ?? await this.#db.engine_ambient_delta_id.get({
@@ -1520,7 +1724,7 @@ class TurnRunner {
1520
1724
  await Promise.all(open.map(({ id }) => this.#liveSubscriptions.cancel(id)));
1521
1725
  }
1522
1726
  async #materializeStreamDeltas(args) {
1523
- const { workspaceId, workerId, loopId, turnId, fromSequence } = args;
1727
+ const { workerId, loopId, turnId, fromSequence } = args;
1524
1728
  const channels = await this.#db.engine_worker_stream_channels.all({ worker_id: workerId });
1525
1729
  let written = 0;
1526
1730
  for (const ch of channels) {
@@ -1529,17 +1733,22 @@ class TurnRunner {
1529
1733
  // ordinary address to the model; only an explicitly non-default
1530
1734
  // channel earns a fragment in the log.
1531
1735
  const visibleFragment = ch.published_channel !== null
1532
- && ch.channel === this.#schemes.defaultChannelFor(ch.runtime, workspaceId)
1736
+ && ch.channel === this.#schemes.defaultChannelFor(ch.runtime, workerId)
1533
1737
  ? null
1534
1738
  : ch.channel;
1535
- const prior = await this.#db.engine_stream_cursor.get({
1536
- worker_id: workerId, scheme: ch.runtime, pathname: ch.coord, fragment: visibleFragment,
1739
+ const streamBase = renderAddress({
1740
+ scheme: ch.runtime,
1741
+ authority: ch.authority,
1742
+ pathname: ch.coord,
1537
1743
  });
1538
- const priorAttrs = prior !== undefined ? JSON.parse(prior.attrs) : {};
1539
- const cursor = priorAttrs.streamEnd ?? 0;
1744
+ const streamTarget = visibleFragment === null
1745
+ ? streamBase
1746
+ : `${streamBase}#${PathSyntax.escapeTarget(visibleFragment)}`;
1747
+ const targetParts = authorityParts(ch.authority);
1748
+ const cursor = ch.published_end;
1540
1749
  const closed = ch.state === "closed" || ch.state === "errored";
1541
1750
  const terminal = closed
1542
- ? Results.assert(JSON.parse(ch.close_result ?? "null"))
1751
+ ? Results.assert(JSON.parse(ch.producer_result ?? "null"))
1543
1752
  : null;
1544
1753
  const terminalResult = async (fields, sequence) => {
1545
1754
  if (terminal === null)
@@ -1567,22 +1776,13 @@ class TurnRunner {
1567
1776
  // content and the auto-OPEN terminal observation never fired — the model was never shown
1568
1777
  // the conclusion of a stream whose result it already holds folded. The same
1569
1778
  // observation is required when the channel produced no publishable content:
1570
- // completion is information independently of payload. Text streams retain their
1571
- // terse marker; structured channels emit a bodyless typed conclusion.
1572
- if (closed && priorAttrs.terminal !== true) {
1573
- const streamTarget = renderTarget({
1574
- scheme: ch.runtime,
1575
- pathname: ch.coord,
1576
- fragment: visibleFragment,
1577
- });
1578
- if (streamTarget === null)
1579
- throw new Error(`stream ${ch.subscription_id} has no renderable address`);
1580
- const pointer = cursor > 0
1581
- ? `full output already delivered above; READ ${streamTarget} to revisit`
1582
- : "stream produced no output";
1779
+ // completion is information independently of payload. A text stream that delivered
1780
+ // content retains its terse revisit marker; an empty text stream and every structured
1781
+ // channel emit a bodyless typed conclusion.
1782
+ if (terminal !== null) {
1583
1783
  const sequence = fromSequence + written;
1584
- const content = baseMimetype(ch.mimetype).startsWith("text/")
1585
- ? `[ stream closed (${ch.close_status ?? 200}) - ${pointer} ]`
1784
+ const content = cursor > 0 && baseMimetype(ch.mimetype).startsWith("text/")
1785
+ ? `[ stream closed (${terminal.status}) - full output already delivered above; READ ${streamTarget} to revisit ]`
1586
1786
  : "";
1587
1787
  const rx = JSON.stringify(await terminalResult({
1588
1788
  content,
@@ -1590,7 +1790,9 @@ class TurnRunner {
1590
1790
  }, sequence));
1591
1791
  await this.#db.engine_insert_stream_delta.run({
1592
1792
  worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence,
1593
- scheme: ch.runtime, pathname: ch.coord, fragment: visibleFragment,
1793
+ subscription_publication_id: ch.publication_id,
1794
+ scheme: ch.runtime, hostname: targetParts.hostname, port: targetParts.port,
1795
+ pathname: ch.coord, fragment: visibleFragment,
1594
1796
  rx,
1595
1797
  weight: LogBody.weight({
1596
1798
  op: "READ",
@@ -1602,7 +1804,7 @@ class TurnRunner {
1602
1804
  }, this.#weighContent),
1603
1805
  status: terminal?.status ?? 200,
1604
1806
  attrs: JSON.stringify({ streamEnd: ch.content.length, terminal: true }),
1605
- expanded: 1,
1807
+ folded: LogVisibility.serialize(LogVisibility.OPEN),
1606
1808
  });
1607
1809
  written++;
1608
1810
  }
@@ -1621,7 +1823,9 @@ class TurnRunner {
1621
1823
  const rx = JSON.stringify(result);
1622
1824
  await this.#db.engine_insert_stream_delta.run({
1623
1825
  worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence,
1624
- scheme: ch.runtime, pathname: ch.coord, fragment: visibleFragment,
1826
+ subscription_publication_id: ch.publication_id,
1827
+ scheme: ch.runtime, hostname: targetParts.hostname, port: targetParts.port,
1828
+ pathname: ch.coord, fragment: visibleFragment,
1625
1829
  rx,
1626
1830
  weight: LogBody.weight({
1627
1831
  op: "READ",
@@ -1633,69 +1837,99 @@ class TurnRunner {
1633
1837
  }, this.#weighContent),
1634
1838
  status: result.status,
1635
1839
  attrs: JSON.stringify({ streamEnd: publishEnd, terminal: terminalDelivery }),
1636
- expanded: terminalDelivery ? 1 : 0, // {§exec-stream} — terminal observation auto-OPENs; ongoing folds
1840
+ folded: LogVisibility.serialize(terminalDelivery ? LogVisibility.OPEN : LogVisibility.FOLDED), // {§exec-stream} — terminal observation auto-OPENs; ongoing folds
1637
1841
  });
1638
1842
  written++;
1639
1843
  }
1640
1844
  return written;
1641
1845
  }
1642
1846
  // {§env-delta-filesystem-narration} {§membership-emi-divergence-signal}
1643
- // — journal project-file divergence once through the reserved actor.
1847
+ // — record project-file divergence once through the reserved actor.
1644
1848
  async #logFsFictions(workspaceId, divergences, gitStatus) {
1645
1849
  if (divergences.length === 0)
1646
1850
  return;
1647
1851
  const gitByPath = new Map(gitStatus?.files.map(({ path, status }) => [path, status]) ?? []);
1648
1852
  const worker = await this.#db.envelope_get_worker_by_name.get({ workspace_id: workspaceId, name: "plurnk" })
1649
- ?? await this.#db.envelope_insert_worker.get({ workspace_id: workspaceId, name: "plurnk", origin: "plurnk" });
1853
+ ?? await this.#db.envelope_insert_worker.get({ workspace_id: workspaceId, name: "plurnk", origin: "_plurnk" });
1650
1854
  if (worker === undefined)
1651
1855
  throw new Error("logFsFictions: plurnk worker resolution returned no row");
1652
1856
  const loop = await this.#db.envelope_insert_client_loop.get({ worker_id: worker.id });
1653
1857
  if (loop === undefined)
1654
1858
  throw new Error("logFsFictions: loop insert returned no row");
1655
- const turn = await JournalTurn.insert(this.#db, loop.id);
1656
- let sequence = 1;
1657
- for (const d of divergences) {
1658
- const span = editedSpan(d.before, d.after);
1659
- const rx = JSON.stringify({ status: 200, entryId: d.entryId, channel: d.channel, span });
1660
- const attrs = gitByPath.has(d.pathname)
1661
- ? JSON.stringify({ git: gitByPath.get(d.pathname) })
1662
- : "{}";
1663
- await this.#db.engine_insert_log_entry.get({
1664
- worker_id: worker.id, loop_id: loop.id, turn_id: turn.id, sequence: sequence++,
1665
- origin: "plurnk", source: "file", model_call_id: null,
1666
- op: "EDIT", suffix: "", signal: null,
1667
- // Match Dispatcher.#extractTarget: a bare file address has NULL scheme
1668
- // only in log target metadata; its entry identity remains `file`.
1669
- scheme: null, username: null, password: null, hostname: null, port: null,
1670
- pathname: d.pathname, query: null, fragment: null, lineMarker: null,
1671
- tx: "", mimetype_tx: "text/plain",
1672
- rx, mimetype_rx: "application/json",
1673
- status_rx: 200,
1674
- weight: LogBody.weight({
1675
- op: "EDIT",
1859
+ const turn = await Turn.open(this.#db, { loopId: loop.id, producer: "_plurnk", kind: "operation" });
1860
+ let turnOpen = true;
1861
+ try {
1862
+ let sequence = 1;
1863
+ for (const d of divergences) {
1864
+ const span = editedSpan(d.before, d.after);
1865
+ const rx = JSON.stringify({ status: 200, entryId: d.entryId, channel: d.channel, span });
1866
+ const attrs = gitByPath.has(d.pathname)
1867
+ ? JSON.stringify({ git: gitByPath.get(d.pathname) })
1868
+ : "{}";
1869
+ await this.#db.engine_insert_log_entry.get({
1870
+ worker_id: worker.id, loop_id: loop.id, turn_id: turn.id, sequence: sequence++,
1871
+ origin: "_plurnk", source: "file", model_call_id: null,
1872
+ op: "EDIT", delimiter: "", signal: null,
1873
+ // Match Dispatcher.#extractTarget: a bare file address has NULL scheme
1874
+ // only in log target metadata; its entry identity remains `file`.
1875
+ scheme: null, username: null, password: null, hostname: null, port: null,
1876
+ pathname: d.pathname, query: null, fragment: null, lineMarker: null,
1877
+ tx: "", mimetype_tx: "text/plain",
1878
+ rx, mimetype_rx: "application/json",
1879
+ status_rx: 200,
1880
+ weight: LogBody.weight({
1881
+ op: "EDIT",
1882
+ attrs,
1883
+ tx: "",
1884
+ rx,
1885
+ mimetypeTx: "text/plain",
1886
+ mimetypeRx: "application/json",
1887
+ }, this.#weighContent),
1888
+ state: "resolved", outcome: null,
1676
1889
  attrs,
1677
- tx: "",
1678
- rx,
1679
- mimetypeTx: "text/plain",
1680
- mimetypeRx: "application/json",
1681
- }, this.#weighContent),
1682
- state: "resolved", outcome: null,
1683
- attrs,
1684
- });
1890
+ });
1891
+ }
1892
+ await Turn.complete(this.#db, turn.id, 200);
1893
+ turnOpen = false;
1894
+ const closed = await new LoopLifecycle(this.#db).finish(loop.id, { status: 200 });
1895
+ if (closed === null)
1896
+ throw new Error(`logFsFictions: narration loop ${loop.id} was not open at completion`);
1897
+ }
1898
+ catch (cause) {
1899
+ const settlementFailures = [];
1900
+ const failure = Results.failure("engine:filesystem-narration", "narration-failed", 500, "Filesystem divergence narration failed before its operation turn settled.", {}, { stage: "filesystem-narration", retryable: false });
1901
+ if (turnOpen) {
1902
+ try {
1903
+ await Turn.complete(this.#db, turn.id, failure.status);
1904
+ }
1905
+ catch (turnCause) {
1906
+ settlementFailures.push(turnCause);
1907
+ }
1908
+ }
1909
+ try {
1910
+ await new LoopLifecycle(this.#db).finish(loop.id, failure);
1911
+ }
1912
+ catch (loopCause) {
1913
+ settlementFailures.push(loopCause);
1914
+ }
1915
+ if (settlementFailures.length > 0) {
1916
+ throw new AggregateError([cause, ...settlementFailures], `filesystem narration ${turn.id} failed to settle`);
1917
+ }
1918
+ throw cause;
1685
1919
  }
1686
1920
  }
1687
- async #writePromptLog({ workerId, loopId, turnId, sequence, target, content, }) {
1921
+ async #writePromptLog({ workerId, loopId, turnId, sequence, target, content, source, }) {
1688
1922
  const rx = JSON.stringify({ content, mimetype: "text/markdown" });
1689
1923
  const row = await this.#db.engine_insert_log_entry.get({
1690
1924
  worker_id: workerId,
1691
1925
  loop_id: loopId,
1692
1926
  turn_id: turnId,
1693
1927
  sequence,
1694
- origin: "plurnk",
1695
- source: null,
1928
+ origin: "_plurnk",
1929
+ source,
1696
1930
  model_call_id: null,
1697
1931
  op: "prompt",
1698
- suffix: "",
1932
+ delimiter: "",
1699
1933
  signal: null,
1700
1934
  scheme: target.scheme,
1701
1935
  username: target.username,