@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
@@ -8,27 +8,36 @@ import ExecutorRegistry from "../core/ExecutorRegistry.js";
8
8
  import SchemeRegistry from "../core/SchemeRegistry.js";
9
9
  import { Mimetypes } from "@plurnk/plurnk-mimetypes";
10
10
  import { RuntimeDeclaration } from "@plurnk/plurnk-execs";
11
+ import { projectModelRoute, routeForSpec, specForRoute } from "./model-route.js";
12
+ import { discoverDaemonModules } from "./module-discovery.js";
13
+ import WorkerSettingsReader from "../core/worker-settings.js";
14
+ import EffectPolicy from "../schemes/EffectPolicy.js";
15
+ import QuestionTool, { questionRuntimeDecl } from "../schemes/QuestionTool.js";
11
16
  import DrainSupervisor from "./DrainSupervisor.js";
12
17
  import { parsePath, Validator, } from "@plurnk/plurnk-contracts";
13
18
  import LogEntry from "./logEntry.js";
14
19
  import Envelope from "./envelope.js";
15
20
  import ClientInput from "./client-input.js";
16
- import JournalTurn from "../core/JournalTurn.js";
17
- import LoopDocs from "./loopDocs.js";
21
+ import Turn from "../core/Turn.js";
22
+ import SkillsFunctionality from "./SkillsFunctionality.js";
18
23
  import GitMembership from "../core/git-membership.js";
19
24
  import Fork from "../core/fork.js";
20
25
  import WorkerName from "../core/WorkerName.js";
26
+ import WorkerControlAddress from "../core/WorkerControlAddress.js";
21
27
  import LoopLifecycle from "../core/LoopLifecycle.js";
22
28
  import { promptLoopPrefix } from "../core/plurnk-uri.js";
23
29
  import { contentWeight } from "../core/content-weight.js";
24
- import { parseAliasesFromEnv, resolveActiveAlias } from "@plurnk/plurnk-providers";
30
+ import { parseAliasesFromEnv, resolveActiveRoute, UnsupportedReasoningPolicyError, } from "@plurnk/plurnk-providers";
25
31
  import ProviderInstantiate from "../core/ProviderInstantiate.js";
26
- import { resolveLoopAlias } from "./loop-model.js";
32
+ import { resolveLoopRoute } from "./loop-model.js";
27
33
  import LoopFlagsReader from "../core/LoopFlagsReader.js";
28
34
  import Results, { OperationFailureError } from "../core/results.js";
29
35
  import WorkspaceGate from "../core/WorkspaceGate.js";
36
+ import WorkerResidency from "./WorkerResidency.js";
30
37
  import BranchBatches from "./BranchBatches.js";
38
+ import Functionality from "./Functionality.js";
31
39
  import { observed, observedSync } from "../observe/spans.js";
40
+ import { listModelCatalog } from "./model-catalog.js";
32
41
  const clientActionFailure = (error) => {
33
42
  if (error instanceof OperationFailureError)
34
43
  return error.result;
@@ -40,6 +49,9 @@ const clientActionFailure = (error) => {
40
49
  };
41
50
  const daemonFailure = (owner, code, status, detail, extensions = {}) => new OperationFailureError(Results.failure(owner, code, status, detail, {}, extensions));
42
51
  const entryReadResult = (result) => Validator.assertEntryReadResult(result);
52
+ const modelRouteLabel = (route) => route.alias === undefined
53
+ ? `model route '${route.provider}/${route.model}'`
54
+ : `provider alias '${route.alias}' (${route.provider}/${route.model})`;
43
55
  export default class Daemon {
44
56
  #db;
45
57
  #engine;
@@ -54,16 +66,17 @@ export default class Daemon {
54
66
  #nodeModulesPath;
55
67
  #discoveryCwd;
56
68
  #started = false; // {§module-lifecycle}: one discovery/module boot; no listener
57
- #capabilitiesPublished = false;
58
69
  #modules = [];
59
70
  #moduleClosers = [];
60
71
  #moduleActions = new Map();
61
- #workspaceCapabilityProviders = new Map();
72
+ #residency;
73
+ #functionality;
74
+ #skills;
62
75
  // {§methods-event-subscribe} — the broadcast's in-process event source. A transport
63
76
  // module (plurnk-agui) subscribes and fans out to its OWN clients; core emits, never owns
64
77
  // client transport or connection state.
65
78
  #eventSubscribers = new Set();
66
- constructor({ db, schemes, mimetypes, provider, nodeModulesPath, }) {
79
+ constructor({ db, schemes, mimetypes, provider, nodeModulesPath, skills, }) {
67
80
  this.#db = db;
68
81
  this.#lifecycle = new LoopLifecycle(db);
69
82
  this.#schemes = schemes ?? new SchemeRegistry();
@@ -83,7 +96,7 @@ export default class Daemon {
83
96
  defaultMimetype: "text/markdown",
84
97
  discoverOptions: { cwd: this.#discoveryCwd },
85
98
  });
86
- const bootSpec = resolveActiveAlias();
99
+ const bootSpec = resolveActiveRoute();
87
100
  if (this.#provider !== null && bootSpec !== null) {
88
101
  ProviderInstantiate.registerInstance(this.#provider, bootSpec);
89
102
  }
@@ -94,6 +107,26 @@ export default class Daemon {
94
107
  });
95
108
  return row !== undefined;
96
109
  });
110
+ // {§module-worker-residency} — residency, activation/cooling, and
111
+ // capability replacement live in their one owner; the Daemon delegates.
112
+ this.#residency = new WorkerResidency({
113
+ db,
114
+ engine: () => this.#engine,
115
+ workspaceGate: this.#workspaceGate,
116
+ normalizeRuntime: (registration) => this.#normalizeRuntime(registration),
117
+ });
118
+ // {§functionality-coordinator} — the one owner above every family
119
+ // adapter; Core hosts it, modules register adapters through the seam.
120
+ this.#functionality = new Functionality({
121
+ registerModuleAction: (registration) => this.registerModuleAction(registration),
122
+ registerWorkerCapabilityProvider: (owner, provider) => this.registerWorkerCapabilityProvider(owner, provider),
123
+ readWorkerModuleState: (workerId, owner) => this.readWorkerModuleState(workerId, owner),
124
+ replaceWorkerCapabilities: (replacement, options) => this.replaceWorkerCapabilities(replacement, options),
125
+ retainWorker: (workerId) => this.#residency.retain(workerId),
126
+ });
127
+ // {§skills-functionality} — Core's own family: standard Agent Skills.
128
+ this.#skills = new SkillsFunctionality({ db, ...skills });
129
+ this.#skills.attach(this.#functionality.register(this.#skills));
97
130
  this.#branchBatches = new BranchBatches(db, this.#workspaceGate, {
98
131
  settleWorkspace: async (workspaceId) => this.#engine.drainWorkspaceDerivations(workspaceId),
99
132
  createChild: async ({ workspaceId, parentWorkerId, parentLoopId, op, name, prompt, flags, origin }) => {
@@ -101,19 +134,31 @@ export default class Daemon {
101
134
  const providerSpec = parentPolicy.childProviderSpec ?? parentPolicy.providerSpec;
102
135
  const workerName = WorkerName.assert(name);
103
136
  const workerId = op === "FORK"
104
- ? await Fork.fork(this.#db, parentWorkerId, workerName)
137
+ ? await Fork.fork(this.#db, parentWorkerId, workerName, (scheme) => this.#schemes.entryInheritanceForStoredScheme(scheme, parentWorkerId))
105
138
  : (await this.#db.fork_insert_worker.get({
106
139
  workspace_id: workspaceId,
107
140
  name: workerName,
108
141
  parent_worker_id: parentWorkerId,
109
142
  origin,
143
+ fork_snapshot: 0,
110
144
  }))?.id;
111
145
  if (workerId === undefined)
112
146
  throw new Error("Branch worker insert returned no row");
147
+ // {§worker-model-selection} — lineage inheritance by value: the branch
148
+ // child begins with the spawning loop's effective spawn model.
149
+ await this.#db.worker_generation_policy_update.run({
150
+ id: workerId,
151
+ model_route_id: await routeForSpec(this.#db, providerSpec),
152
+ spawn_model_route_id: null,
153
+ reasoning_policy: parentPolicy.reasoningPolicy,
154
+ });
155
+ const source = await this.#workerPromptSource(workspaceId, parentWorkerId, workerId);
113
156
  const loopId = await this.#drains.enqueueFreshLoop({
114
157
  workerId,
115
158
  prompt,
159
+ ...(source === undefined ? {} : { source }),
116
160
  providerSpec,
161
+ reasoningPolicy: parentPolicy.reasoningPolicy,
117
162
  childProviderSpec: parentPolicy.childProviderSpec,
118
163
  flags,
119
164
  });
@@ -145,28 +190,56 @@ export default class Daemon {
145
190
  // model-independent. Request-shaped token facts stay provider-owned.
146
191
  weigh: contentWeight,
147
192
  streamEventNotify: (workspaceId, event) => this.notifyStreamEvent(workspaceId, event),
193
+ reasoningEventNotify: (workspaceId, event) => this.notifyReasoningEvent(workspaceId, event),
148
194
  wakeWorkerNotify: (payload) => this.#drains.notifyWakeWorker(payload),
149
195
  // worker:// loop-start primitive — spawn/fork/irc deliver through
150
196
  // Daemon.inject (active sister → fold; idle → enqueue + drain). The
151
197
  // daemon owns provider + the law-file system prompt; the worker scheme
152
198
  // handler carries neither. Fire-and-forget: the returned drain runs
153
199
  // independently (the sister is its own worker). {§machine-processes}
154
- injectWorker: async ({ workspaceId, workerId, prompt, flags, parentLoopId }) => {
200
+ injectWorker: async ({ workspaceId, workerId, sourceWorkerId, prompt, flags, parentLoopId }) => {
201
+ await this.#assertModelWorker(workspaceId, workerId);
202
+ const source = await this.#workerPromptSource(workspaceId, sourceWorkerId, workerId);
155
203
  const systemPrompt = await readFile(Paths.instructionsSystem, "utf8");
156
- const parentPolicy = parentLoopId === undefined
157
- ? null
158
- : await this.#providerPolicyForLoop(parentLoopId);
159
- const providerSpec = parentPolicy === null
160
- ? resolveActiveAlias()
161
- : parentPolicy.childProviderSpec ?? parentPolicy.providerSpec;
162
- if (providerSpec === null)
163
- throw new Error("injectWorker: active provider has no resolvable alias");
204
+ let providerSpec;
205
+ let childProviderSpec;
206
+ let reasoningPolicy;
207
+ if (parentLoopId === undefined) {
208
+ // {§worker-model-selection} — the voice door addresses an
209
+ // existing worker. Its own durable generation policy is the
210
+ // receiver's identity; the daemon default and sender do not
211
+ // overwrite it merely because a new loop must be minted.
212
+ const targetPolicy = await this.#resolveWorkerModel(workerId, undefined);
213
+ if (targetPolicy === null) {
214
+ throw new Error(`injectWorker: worker ${workerId} has no resolvable model route`);
215
+ }
216
+ providerSpec = targetPolicy.providerSpec;
217
+ reasoningPolicy = targetPolicy.reasoningPolicy;
218
+ childProviderSpec = await this.#resolveWorkerSpawnModel(workerId, undefined);
219
+ }
220
+ else {
221
+ const parentPolicy = await this.#providerPolicyForLoop(parentLoopId);
222
+ providerSpec = parentPolicy.childProviderSpec ?? parentPolicy.providerSpec;
223
+ reasoningPolicy = parentPolicy.reasoningPolicy;
224
+ // {§worker-model-selection} — lineage inheritance by value:
225
+ // the child begins with the spawning loop's effective spawn
226
+ // model and no separate override of its own.
227
+ childProviderSpec = null;
228
+ await this.#db.worker_generation_policy_update.run({
229
+ id: workerId,
230
+ model_route_id: await routeForSpec(this.#db, providerSpec),
231
+ spawn_model_route_id: null,
232
+ reasoning_policy: reasoningPolicy,
233
+ });
234
+ }
164
235
  const { action, loopId } = await this.inject({
165
236
  workspaceId,
166
237
  workerId,
167
238
  prompt,
239
+ ...(source === undefined ? {} : { source }),
168
240
  providerSpec,
169
- ...(parentPolicy === null ? {} : { childProviderSpec: parentPolicy.childProviderSpec }),
241
+ reasoningPolicy,
242
+ childProviderSpec,
170
243
  systemPrompt,
171
244
  ...(flags === undefined ? {} : { flags }),
172
245
  });
@@ -175,7 +248,15 @@ export default class Daemon {
175
248
  branchWorker: async (args) => this.#branchBatches.enqueue(args),
176
249
  branchCompletionGate: async (workerId) => this.#branchBatches.completionGate(workerId),
177
250
  acquireWorkspaceTurn: async (workspaceId, workerId) => this.#workspaceGate.acquireTurn(workspaceId, workerId),
178
- workspaceTurnCompleted: async ({ turnId }) => this.#branchBatches.sealTurn(turnId),
251
+ // {§skills-hotload} — filesystem installers operate out of band.
252
+ // Republish under the workspace turn gate before packet assembly so
253
+ // the first subsequent model turn sees their exact result.
254
+ workspaceTurnStarting: async ({ workspaceId, workerId }) => {
255
+ await this.#skills.refreshIfChanged({ workspaceId, workerId });
256
+ },
257
+ workspaceTurnCompleted: async ({ turnId }) => {
258
+ await this.#branchBatches.sealTurn(turnId);
259
+ },
179
260
  // worker:// KILL (terminate) — cancel the addressed worker subtree and
180
261
  // tear down its held streams before the operation completes.
181
262
  cancelWorker: async (workerId, reason) => this.#drains.cancelWorkerTree(workerId, reason),
@@ -185,10 +266,15 @@ export default class Daemon {
185
266
  this.#drains = new DrainSupervisor({
186
267
  db,
187
268
  lifecycle: this.#lifecycle,
188
- injectPrompt: (workerId, prompt, openPaths) => this.#engine.inject(workerId, prompt, openPaths),
189
- assertInjectionCompatibility: async ({ workerId, loopId, providerSpec, childProviderSpec, turnCeiling, flags, }) => {
269
+ injectPrompt: (workerId, prompt, openPaths, source) => this.#engine.inject(workerId, prompt, openPaths, source),
270
+ assertInjectionCompatibility: async ({ workerId, loopId, providerSpec, providerSpecExplicit, reasoningPolicy, childProviderSpec, turnCeiling, flags, }) => {
190
271
  await this.#assertFoldPosture(workerId, flags, loopId);
191
- await this.#assertLoopProvider(loopId, providerSpec);
272
+ // An omitted selector keeps the loop's durable provider; only an
273
+ // explicit selection is checked against it (a deliberate switch).
274
+ if (providerSpecExplicit !== false) {
275
+ await this.#assertLoopProvider(loopId, providerSpec);
276
+ }
277
+ await this.#assertLoopReasoningPolicy(loopId, reasoningPolicy);
192
278
  if (childProviderSpec !== undefined) {
193
279
  await this.#assertLoopChildProvider(loopId, childProviderSpec);
194
280
  }
@@ -196,22 +282,34 @@ export default class Daemon {
196
282
  },
197
283
  reconcilePrompts: (workerId, endedLoopId) => this.#reconcileOrphanedPrompts(workerId, endedLoopId),
198
284
  runLoop: async ({ workspaceId, workerId, loopId, maxTurns, prompt, systemPrompt, signal, onDispatch, }) => {
199
- const { provider, childProvider } = await this.#providersForLoop(loopId);
200
- return this.#engine.runLoop({
201
- provider,
202
- childProvider,
203
- workspaceId,
204
- workerId,
205
- loopId,
206
- maxTurns,
207
- messages: [
208
- { role: "system", content: systemPrompt },
209
- { role: "user", content: prompt },
210
- ],
211
- origin: "model",
212
- signal,
213
- onDispatch,
214
- });
285
+ // The drain callback is the final provider/model boundary. Every
286
+ // admission path, including boot recovery, terminates here.
287
+ await this.#assertModelWorker(workspaceId, workerId);
288
+ const releaseCapabilities = await this.#residency.acquire(workspaceId, workerId);
289
+ try {
290
+ // Worker settings can change while Functionality remains
291
+ // resident. Reconcile the worker-private discovery surface
292
+ // at the same pre-inference boundary used for activation.
293
+ await this.#residency.reconcile(workspaceId, workerId);
294
+ const { provider, childProvider } = await this.#providersForLoop(loopId);
295
+ return await this.#engine.runLoop({
296
+ provider,
297
+ childProvider,
298
+ workspaceId,
299
+ workerId,
300
+ loopId,
301
+ maxTurns,
302
+ messages: [
303
+ { role: "system", content: systemPrompt },
304
+ { role: "user", content: prompt },
305
+ ],
306
+ signal,
307
+ onDispatch,
308
+ });
309
+ }
310
+ finally {
311
+ releaseCapabilities();
312
+ }
215
313
  },
216
314
  loopUsage: (loopId) => this.#engine.loopUsage(loopId),
217
315
  loopAttributions: (loopId) => this.#engine.loopAttributions(loopId),
@@ -220,7 +318,6 @@ export default class Daemon {
220
318
  this.#engine.parkDeadlines.delete(loopId);
221
319
  return deadline;
222
320
  },
223
- settleSearch: (target, status) => this.#engine.searchGate.settle(target, status),
224
321
  cancelSubscription: (subscriptionId) => this.#engine.cancelSubscription(subscriptionId),
225
322
  hasActiveStreams: (workerId) => this.#workerHasActiveStreams(workerId),
226
323
  readSystemPrompt: () => readFile(Paths.instructionsSystem, "utf8"),
@@ -279,60 +376,31 @@ export default class Daemon {
279
376
  async runLoop(args) {
280
377
  const workspaceId = ClientInput.assertId("runLoop", "workspaceId", args.workspaceId);
281
378
  const workerId = ClientInput.assertId("runLoop", "workerId", args.workerId);
379
+ await this.#assertModelWorker(workspaceId, workerId);
282
380
  const prompt = ClientInput.assertPrompt("runLoop", args.prompt);
381
+ const source = ClientInput.assertOptionalSource("runLoop", args.source);
283
382
  const requestedMaxTurns = ClientInput.assertMaxTurns("runLoop", args.maxTurns);
284
383
  const openPaths = ClientInput.assertOpenPaths("runLoop", args.openPaths);
285
- const alias = ClientInput.assertOptionalSelector("runLoop", "alias", args.alias);
286
- const model = ClientInput.assertOptionalSelector("runLoop", "model", args.model);
287
- const childAlias = ClientInput.assertOptionalChildAlias("runLoop", args.childAlias);
288
- const childModel = ClientInput.assertOptionalSelector("runLoop", "childModel", args.childModel);
289
- if (childAlias === null && childModel !== undefined) {
290
- throw daemonFailure("daemon:input", "child-provider-conflict", 400, "childModel cannot accompany an inherited child provider policy.", { field: "childModel", recovery: "Omit childModel or select a child alias.", retryable: false });
291
- }
384
+ const selector = ClientInput.assertOptionalSelector("runLoop", "selector", args.selector);
385
+ const childSelector = ClientInput.assertOptionalChildSelector("runLoop", args.childSelector);
292
386
  const flags = ClientInput.normalizeLoopFlags("runLoop", args.flags);
293
- // {§methods-loop-run-model} — a client sends alias/model on every loop, so a
294
- // switch takes effect turn-to-turn. `model` (client-resolved <provider>/<model>) wins
295
- // over `alias`; neither → the boot default. Instantiation is cached, so ping-ponging
296
- // between two models is cheap, and an unresolvable alias/model fails loud here.
297
- const selection = await this.#resolveLoopProvider(alias, model);
298
- if (selection === null) {
299
- throw new OperationFailureError(Results.failure("daemon:provider", "not-configured", 501, "No provider is configured for this loop.", {}, {
387
+ // {§worker-model-selection} — the worker owns the model. An explicit selector
388
+ // persists onto the worker; an omitted selector resolves the worker's durable model
389
+ // (seeded once from the daemon default). The loop then snapshots the resolved route.
390
+ // A continuation's omitted selector must still keep the loop's durable provider.
391
+ const selectorExplicit = selector !== undefined;
392
+ const generationPolicy = await this.#resolveWorkerModel(workerId, selector);
393
+ if (generationPolicy === null) {
394
+ throw new OperationFailureError(Results.failure("daemon:provider", "not-configured", 501, "No provider is configured for this worker.", {}, {
300
395
  stage: "provider-selection",
301
396
  recovery: "Select a configured model provider.",
302
397
  retryable: false,
303
398
  }));
304
399
  }
305
- // {§methods-loop-run-child-provider}
306
- const configuredChildAlias = childAlias === undefined && childModel === undefined
307
- ? process.env.PLURNK_MODEL_CHILD
308
- : childAlias;
309
- const childSelection = configuredChildAlias === null
310
- || (configuredChildAlias === undefined && childModel === undefined)
311
- ? null
312
- : await this.#resolveLoopProvider(configuredChildAlias, childModel);
400
+ const { providerSpec: selection, reasoningPolicy } = generationPolicy;
401
+ // {§methods-loop-run-child-provider} — the worker's persistent spawn override.
402
+ const childSelection = await this.#resolveWorkerSpawnModel(workerId, childSelector);
313
403
  const systemPrompt = await readFile(Paths.instructionsSystem, "utf8");
314
- // {§machine-processes} — the model NEVER runs in a client-origin worker (its packets would carry
315
- // client-action rows). The module resolves the model worker via ensureModelWorker and passes it (or a
316
- // fork); a client worker here is a caller error, refused loudly rather than silently rehomed.
317
- const target = await this.#db.envelope_get_worker_by_id.get({ id: workerId });
318
- if (target === undefined) {
319
- throw daemonFailure("daemon:worker", "worker-not-found", 404, `Worker ${workerId} does not exist.`, { workerId });
320
- }
321
- if (target.workspace_id !== workspaceId) {
322
- throw daemonFailure("daemon:worker", "workspace-mismatch", 409, `Worker ${workerId} does not belong to workspace ${workspaceId}.`, {
323
- workerId,
324
- workspaceId,
325
- actualWorkspaceId: target.workspace_id,
326
- retryable: false,
327
- });
328
- }
329
- if (target.origin === "client") {
330
- throw daemonFailure("daemon:worker", "model-worker-required", 409, `Worker ${workerId} is not a model worker.`, {
331
- workerId,
332
- recovery: "Select or create a model worker for this loop.",
333
- retryable: false,
334
- });
335
- }
336
404
  // {§operator-config-max-turns-ceiling} — the operator ceiling clamps a per-call maxTurns; a
337
405
  // seam caller must not bypass operator policy (inject only DEFAULTS from env, never clamps).
338
406
  const ceiling = Number(process.env.PLURNK_SERVICE_MAX_TURNS ?? "-1");
@@ -346,76 +414,187 @@ export default class Daemon {
346
414
  workspaceId,
347
415
  workerId,
348
416
  prompt,
417
+ ...(source === undefined ? {} : { source }),
349
418
  ...(flags !== undefined ? { flags } : {}),
350
419
  ...(openPaths !== undefined ? { openPaths } : {}),
351
420
  turnCeiling,
352
421
  providerSpec: selection,
422
+ providerSpecExplicit: selectorExplicit,
423
+ reasoningPolicy,
353
424
  childProviderSpec: childSelection,
354
425
  systemPrompt,
355
426
  });
356
427
  return { status: 100, action, loopId, ...(turnSeq !== undefined ? { turnSeq } : {}) };
357
428
  }
358
- // {§methods-loop-run-model} — resolve a per-loop model override to a cached Provider. `model`
359
- // (<provider>/<model>, client-resolved) wins over a named `alias`; absent both, the
360
- // boot default. A named alias missing from the env cascade, or a malformed model spec, throws
361
- // legibly rather than silently running the wrong model.
362
- async #resolveLoopProvider(alias, model) {
363
- const requested = resolveLoopAlias(alias, model, parseAliasesFromEnv());
429
+ // {§methods-loop-run-model} — resolve one alias-or-route selector to a cached
430
+ // Provider; absent uses the boot default. An unknown alias or malformed exact
431
+ // route throws legibly rather than silently running the wrong model.
432
+ async #resolveLoopProvider(selector, reasoningPolicy) {
433
+ const requested = resolveLoopRoute(selector, parseAliasesFromEnv());
364
434
  if (requested === null && this.#provider === null)
365
435
  return null;
366
- const spec = requested ?? resolveActiveAlias();
436
+ const spec = requested ?? resolveActiveRoute();
367
437
  if (spec === null) {
368
- throw daemonFailure("daemon:provider", "active-alias-unresolved", 500, "The active provider has no resolvable alias.", { stage: "provider-selection", retryable: false });
438
+ throw daemonFailure("daemon:provider", "active-model-unresolved", 500, "The active provider has no resolvable model route.", { stage: "provider-selection", retryable: false });
369
439
  }
370
- // Resolve eagerly so runLoop fails before enqueue when the provider
371
- // cannot be constructed. The drain later retrieves this cached handle
372
- // from the loop's durable spec at the claim boundary.
440
+ await this.#providerForPolicy(spec, reasoningPolicy);
441
+ return spec;
442
+ }
443
+ // Resolve eagerly so runLoop fails before enqueue when the selected route
444
+ // and durable reasoning policy cannot compose. The drain later retrieves
445
+ // this cached handle from the loop's immutable snapshot.
446
+ async #providerForPolicy(spec, reasoningPolicy) {
373
447
  try {
374
- await ProviderInstantiate.instantiateProvider(spec);
448
+ const provider = await ProviderInstantiate.instantiateProvider(spec, process.env, reasoningPolicy);
449
+ ProviderInstantiate.validateGrammarConfiguration(provider, process.env, reasoningPolicy);
450
+ return provider;
375
451
  }
376
452
  catch (cause) {
377
453
  if (cause instanceof OperationFailureError)
378
454
  throw cause;
379
- console.error(`Provider alias '${spec.alias}' could not be instantiated:`, cause);
380
- throw daemonFailure("daemon:provider", "provider-unavailable", 503, `Provider alias '${spec.alias}' is unavailable.`, {
381
- alias: spec.alias,
455
+ if (cause instanceof UnsupportedReasoningPolicyError) {
456
+ throw daemonFailure("daemon:provider", "reasoning-policy-unsupported", 409, `${modelRouteLabel(spec)} does not support reasoning policy '${cause.policy}'.`, {
457
+ ...(spec.alias === undefined ? {} : { alias: spec.alias }),
458
+ provider: spec.provider,
459
+ model: spec.model,
460
+ reasoningPolicy: cause.policy,
461
+ supportedReasoningPolicies: cause.supported,
462
+ stage: "provider-selection",
463
+ recovery: "Select one of the provider's supported reasoning policies.",
464
+ retryable: false,
465
+ });
466
+ }
467
+ console.error(`${modelRouteLabel(spec)} could not be instantiated:`, cause);
468
+ throw daemonFailure("daemon:provider", "provider-unavailable", 503, `${modelRouteLabel(spec)} is unavailable.`, {
469
+ ...(spec.alias === undefined ? {} : { alias: spec.alias }),
382
470
  provider: spec.provider,
383
471
  model: spec.model,
384
472
  stage: "provider-selection",
385
473
  retryable: false,
386
474
  });
387
475
  }
388
- return spec;
389
476
  }
390
- #parseProviderSpec(loopId, field, encoded) {
391
- let parsed;
392
- try {
393
- parsed = JSON.parse(encoded);
477
+ // {§worker-model-selection} — a model worker owns one durable model. An explicit
478
+ // selector persists onto the worker; an omitted selector resolves the worker's
479
+ // durable model, seeded once from the daemon default. A deliberately modelless
480
+ // daemon leaves the worker unset until an explicit selection arrives.
481
+ async #resolveWorkerModel(workerId, selector) {
482
+ const worker = await this.#db.worker_generation_policy_read.get({ id: workerId });
483
+ if (worker === undefined)
484
+ throw new Error(`worker ${workerId}: model route row missing`);
485
+ if (selector !== undefined) {
486
+ const spec = await this.#resolveLoopProvider(selector, worker.reasoning_policy ?? undefined);
487
+ if (spec === null)
488
+ return null;
489
+ const reasoningPolicy = worker.reasoning_policy
490
+ ?? ProviderInstantiate.configuredReasoningPolicy(spec);
491
+ if (worker.spawn_model_route_id !== null) {
492
+ const spawnSpec = await specForRoute(this.#db, worker.spawn_model_route_id);
493
+ if (spawnSpec === null)
494
+ throw new Error(`worker ${workerId}: spawn model route is missing`);
495
+ await this.#providerForPolicy(spawnSpec, reasoningPolicy);
496
+ }
497
+ await this.#db.worker_generation_policy_update.run({
498
+ id: workerId,
499
+ model_route_id: await routeForSpec(this.#db, spec),
500
+ spawn_model_route_id: worker.spawn_model_route_id,
501
+ reasoning_policy: reasoningPolicy,
502
+ });
503
+ return { providerSpec: spec, reasoningPolicy };
394
504
  }
395
- catch {
396
- throw new Error(`loop ${loopId}: persisted ${field} is malformed`);
505
+ if (worker.model_route_id !== null) {
506
+ if (worker.reasoning_policy === null) {
507
+ throw new Error(`worker ${workerId}: durable model has no reasoning policy`);
508
+ }
509
+ const spec = await specForRoute(this.#db, worker.model_route_id);
510
+ if (spec === null)
511
+ throw new Error(`worker ${workerId}: model route is missing`);
512
+ await this.#providerForPolicy(spec, worker.reasoning_policy);
513
+ return { providerSpec: spec, reasoningPolicy: worker.reasoning_policy };
397
514
  }
398
- if (parsed === null)
515
+ if (this.#provider === null)
399
516
  return null;
400
- if (typeof parsed.alias !== "string" || parsed.alias.length === 0
401
- || typeof parsed.provider !== "string" || parsed.provider.length === 0
402
- || typeof parsed.model !== "string" || parsed.model.length === 0
403
- || (parsed.baseUrl !== undefined && typeof parsed.baseUrl !== "string")) {
404
- throw new Error(`loop ${loopId}: persisted ${field} is invalid`);
517
+ const spec = resolveActiveRoute();
518
+ if (spec !== null) {
519
+ const reasoningPolicy = ProviderInstantiate.configuredReasoningPolicy(spec);
520
+ await this.#providerForPolicy(spec, reasoningPolicy);
521
+ if (worker.spawn_model_route_id !== null) {
522
+ const spawnSpec = await specForRoute(this.#db, worker.spawn_model_route_id);
523
+ if (spawnSpec === null)
524
+ throw new Error(`worker ${workerId}: spawn model route is missing`);
525
+ await this.#providerForPolicy(spawnSpec, reasoningPolicy);
526
+ }
527
+ await this.#db.worker_generation_policy_update.run({
528
+ id: workerId,
529
+ model_route_id: await routeForSpec(this.#db, spec),
530
+ spawn_model_route_id: worker.spawn_model_route_id,
531
+ reasoning_policy: reasoningPolicy,
532
+ });
533
+ return { providerSpec: spec, reasoningPolicy };
405
534
  }
406
- return parsed;
535
+ return null;
407
536
  }
537
+ // {§worker-model-selection} — the persistent spawn override. An explicit child
538
+ // selector persists onto the worker (null clears it back to inherit); an omitted
539
+ // selector resolves the persisted override, seeded once from the operator's
540
+ // PLURNK_MODEL_CHILD default.
541
+ async #resolveWorkerSpawnModel(workerId, childSelector) {
542
+ const worker = await this.#db.worker_generation_policy_read.get({ id: workerId });
543
+ if (worker === undefined)
544
+ throw new Error(`worker ${workerId}: model route row missing`);
545
+ if (childSelector !== undefined) {
546
+ const spec = childSelector === null
547
+ ? null
548
+ : await this.#resolveLoopProvider(childSelector, worker.reasoning_policy ?? undefined);
549
+ await this.#db.worker_generation_policy_update.run({
550
+ id: workerId,
551
+ model_route_id: worker.model_route_id,
552
+ spawn_model_route_id: spec === null ? null : await routeForSpec(this.#db, spec),
553
+ reasoning_policy: worker.reasoning_policy,
554
+ });
555
+ return spec;
556
+ }
557
+ if (worker.spawn_model_route_id !== null) {
558
+ const spec = await specForRoute(this.#db, worker.spawn_model_route_id);
559
+ if (spec === null)
560
+ throw new Error(`worker ${workerId}: spawn model route is missing`);
561
+ if (worker.reasoning_policy !== null) {
562
+ await this.#providerForPolicy(spec, worker.reasoning_policy);
563
+ }
564
+ return spec;
565
+ }
566
+ const configured = process.env.PLURNK_MODEL_CHILD;
567
+ if (configured === undefined || configured.length === 0)
568
+ return null;
569
+ const spec = await this.#resolveLoopProvider(configured, worker.reasoning_policy ?? undefined);
570
+ if (spec !== null) {
571
+ await this.#db.worker_generation_policy_update.run({
572
+ id: workerId,
573
+ model_route_id: worker.model_route_id,
574
+ spawn_model_route_id: await routeForSpec(this.#db, spec),
575
+ reasoning_policy: worker.reasoning_policy,
576
+ });
577
+ }
578
+ return spec;
579
+ }
580
+ // {§worker-model-selection} — the loop's durable snapshot is the immutable route ids,
581
+ // resolved through model_routes at the claim boundary; never re-resolved through the
582
+ // alias cascade (a changed alias declaration must not rewrite history).
408
583
  async #providerPolicyForLoop(loopId) {
409
- const row = await this.#db.drain_loop_provider_spec.get({ loop_id: loopId });
584
+ const row = await this.#db.drain_loop_generation_policy.get({ loop_id: loopId });
410
585
  if (row === undefined)
411
586
  throw new Error(`loop ${loopId}: provider selection row is missing`);
412
- const providerSpec = this.#parseProviderSpec(loopId, "provider_spec", row.provider_spec);
587
+ const providerSpec = await specForRoute(this.#db, row.model_route_id);
413
588
  if (providerSpec === null) {
414
- throw new Error(`loop ${loopId}: persisted provider selection is missing or invalid — refusing boot-default substitution`);
589
+ throw new Error(`loop ${loopId}: persisted provider selection is missing — refusing boot-default substitution`);
590
+ }
591
+ if (row.reasoning_policy === null) {
592
+ throw new Error(`loop ${loopId}: persisted reasoning policy is missing`);
415
593
  }
416
594
  return {
417
595
  providerSpec,
418
- childProviderSpec: this.#parseProviderSpec(loopId, "child_provider_spec", row.child_provider_spec),
596
+ childProviderSpec: await specForRoute(this.#db, row.spawn_model_route_id),
597
+ reasoningPolicy: row.reasoning_policy,
419
598
  };
420
599
  }
421
600
  async #providerSpecForLoop(loopId) {
@@ -423,20 +602,20 @@ export default class Daemon {
423
602
  }
424
603
  async #providersForLoop(loopId) {
425
604
  const policy = await this.#providerPolicyForLoop(loopId);
426
- const provider = await ProviderInstantiate.instantiateProvider(policy.providerSpec);
605
+ const provider = await this.#providerForPolicy(policy.providerSpec, policy.reasoningPolicy);
427
606
  const childProvider = policy.childProviderSpec === null
428
607
  ? provider
429
- : await ProviderInstantiate.instantiateProvider(policy.childProviderSpec);
608
+ : await this.#providerForPolicy(policy.childProviderSpec, policy.reasoningPolicy);
430
609
  return { provider, childProvider };
431
610
  }
432
611
  async #assertLoopProvider(loopId, requested) {
433
612
  const selected = await this.#providerSpecForLoop(loopId);
434
613
  if (JSON.stringify(selected) !== JSON.stringify(requested)) {
435
- throw daemonFailure("daemon:provider", "loop-provider-conflict", 409, `Loop ${loopId} uses provider alias '${selected.alias}', not '${requested.alias}'.`, {
614
+ throw daemonFailure("daemon:provider", "loop-provider-conflict", 409, `Loop ${loopId} uses ${modelRouteLabel(selected)}, not ${modelRouteLabel(requested)}.`, {
436
615
  loopId,
437
- selectedAlias: selected.alias,
616
+ ...(selected.alias === undefined ? {} : { selectedAlias: selected.alias }),
438
617
  selectedModel: `${selected.provider}/${selected.model}`,
439
- requestedAlias: requested.alias,
618
+ ...(requested.alias === undefined ? {} : { requestedAlias: requested.alias }),
440
619
  requestedModel: `${requested.provider}/${requested.model}`,
441
620
  stage: "loop-injection",
442
621
  recovery: "Cancel or conclude the loop before selecting another provider.",
@@ -444,13 +623,28 @@ export default class Daemon {
444
623
  });
445
624
  }
446
625
  }
626
+ async #assertLoopReasoningPolicy(loopId, requested) {
627
+ const selected = (await this.#providerPolicyForLoop(loopId)).reasoningPolicy;
628
+ if (selected !== requested) {
629
+ throw daemonFailure("daemon:provider", "loop-reasoning-policy-conflict", 409, `Loop ${loopId} uses reasoning policy '${selected}', not '${requested}'.`, {
630
+ loopId,
631
+ selectedReasoningPolicy: selected,
632
+ requestedReasoningPolicy: requested,
633
+ stage: "loop-injection",
634
+ recovery: "Cancel or conclude the loop before selecting another reasoning policy.",
635
+ retryable: false,
636
+ });
637
+ }
638
+ }
447
639
  async #assertLoopChildProvider(loopId, requested) {
448
640
  const selected = (await this.#providerPolicyForLoop(loopId)).childProviderSpec;
449
641
  if (JSON.stringify(selected) !== JSON.stringify(requested)) {
450
642
  throw daemonFailure("daemon:provider", "loop-child-provider-conflict", 409, `Loop ${loopId} already has a different child provider policy.`, {
451
643
  loopId,
452
644
  selectedChildAlias: selected?.alias ?? null,
645
+ selectedChildModel: selected === null ? null : `${selected.provider}/${selected.model}`,
453
646
  requestedChildAlias: requested?.alias ?? null,
647
+ requestedChildModel: requested === null ? null : `${requested.provider}/${requested.model}`,
454
648
  stage: "loop-injection",
455
649
  recovery: "Cancel or conclude the loop before changing its child provider policy.",
456
650
  retryable: false,
@@ -476,69 +670,316 @@ export default class Daemon {
476
670
  }
477
671
  // {§methods-model-worker} — the workspace's model worker (created on first use), distinct from the client
478
672
  // worker so the model's packets never carry client-action rows. The module binds its threads to this.
479
- ensureModelWorker(workspaceId) {
480
- return Envelope.ensureModelWorker(this.#db, ClientInput.assertId("worker.ensure-model", "workspaceId", workspaceId));
673
+ // Optional worker settings ({§worker-settings}) ride the client's per-run declaration: merged in on
674
+ // creation AND on every subsequent ensure, so a client can change its mind between loops.
675
+ async ensureModelWorker(workspaceId, settings) {
676
+ const checked = ClientInput.assertId("worker.ensure-model", "workspaceId", workspaceId);
677
+ const created = await Envelope.ensureModelWorker(this.#db, checked);
678
+ if (settings !== undefined)
679
+ await this.#mergeWorkerSettings(created, settings);
680
+ return created;
681
+ }
682
+ // {§worker-settings} — merge known keys into the worker's behavioral-rules bag.
683
+ // Validated at the boundary; unprovided keys keep their durable value.
684
+ async #mergeWorkerSettings(workerId, settings) {
685
+ const current = await WorkerSettingsReader.read(this.#db, workerId);
686
+ const merged = {
687
+ requestUserInput: settings.requestUserInput ?? current.requestUserInput,
688
+ };
689
+ await this.#db.worker_settings_update.run({ id: workerId, settings: JSON.stringify(merged) });
690
+ }
691
+ // {§worker-settings} — project the worker's behavioral rules for a client-interface surface.
692
+ async readWorkerSettings(args) {
693
+ const workspaceId = ClientInput.assertId("worker.settings.get", "workspaceId", args.workspaceId);
694
+ const workerId = ClientInput.assertId("worker.settings.get", "workerId", args.workerId);
695
+ await this.#assertWorkerOwned(workspaceId, workerId);
696
+ const settings = await WorkerSettingsReader.read(this.#db, workerId);
697
+ return { requestUserInput: settings.requestUserInput };
698
+ }
699
+ // {§worker-settings} — persist known behavioral-rule keys; unknown keys are
700
+ // rejected at the input boundary, never persisted.
701
+ async setWorkerSettings(args) {
702
+ const workspaceId = ClientInput.assertId("worker.settings.set", "workspaceId", args.workspaceId);
703
+ const workerId = ClientInput.assertId("worker.settings.set", "workerId", args.workerId);
704
+ await this.#assertWorkerOwned(workspaceId, workerId);
705
+ ClientInput.parseWorkerSettings(args.settings);
706
+ await this.#mergeWorkerSettings(workerId, args.settings);
707
+ const settings = await WorkerSettingsReader.read(this.#db, workerId);
708
+ return { requestUserInput: settings.requestUserInput };
709
+ }
710
+ // {§worker-model-selection} — project a worker's durable model and spawn override
711
+ // for a client-interface get/set surface. Ownership is checked at this boundary.
712
+ async readWorkerModel(args) {
713
+ const workspaceId = ClientInput.assertId("worker.model.get", "workspaceId", args.workspaceId);
714
+ const workerId = ClientInput.assertId("worker.model.get", "workerId", args.workerId);
715
+ await this.#assertWorkerOwned(workspaceId, workerId);
716
+ const row = await this.#db.worker_generation_policy_read.get({ id: workerId });
717
+ if (row === undefined)
718
+ throw new Error(`worker ${workerId}: model route row missing`);
719
+ const modelSpec = await specForRoute(this.#db, row.model_route_id);
720
+ const spawnModelSpec = await specForRoute(this.#db, row.spawn_model_route_id);
721
+ return {
722
+ model: modelSpec === null ? null : projectModelRoute(modelSpec),
723
+ spawnModel: spawnModelSpec === null ? null : projectModelRoute(spawnModelSpec),
724
+ };
725
+ }
726
+ // {§worker-model-selection} — persist an explicit model selection onto the worker
727
+ // and return the resolved spec. An unresolvable selector fails loud here.
728
+ async setWorkerModel(args) {
729
+ const workspaceId = ClientInput.assertId("worker.model.set", "workspaceId", args.workspaceId);
730
+ const workerId = ClientInput.assertId("worker.model.set", "workerId", args.workerId);
731
+ await this.#assertWorkerOwned(workspaceId, workerId);
732
+ await this.#assertWorkerSelectable(workerId);
733
+ const selector = ClientInput.assertSelector("worker.model.set", "selector", args.selector);
734
+ const policy = await this.#resolveWorkerModel(workerId, selector);
735
+ if (policy === null) {
736
+ throw new OperationFailureError(Results.failure("daemon:provider", "not-configured", 501, "No provider is configured for this worker.", {}, { stage: "provider-selection", recovery: "Select a configured model provider.", retryable: false }));
737
+ }
738
+ return projectModelRoute(policy.providerSpec);
739
+ }
740
+ async #reasoningSupportForWorker(workerId, row, providerSpec, policy) {
741
+ const provider = await this.#providerForPolicy(providerSpec, policy);
742
+ if (row.spawn_model_route_id === null)
743
+ return provider.supportedReasoningPolicies;
744
+ const spawnSpec = await specForRoute(this.#db, row.spawn_model_route_id);
745
+ if (spawnSpec === null)
746
+ throw new Error(`worker ${workerId}: spawn model route is missing`);
747
+ const spawnProvider = await this.#providerForPolicy(spawnSpec, policy);
748
+ return provider.supportedReasoningPolicies.filter((candidate) => spawnProvider.supportedReasoningPolicies.includes(candidate));
749
+ }
750
+ // {§worker-reasoning-policy} — the worker's reasoning policy is durable and
751
+ // provider-relative. A model-less worker remains explicitly uninitialized.
752
+ async readWorkerReasoning(args) {
753
+ const workspaceId = ClientInput.assertId("worker.reasoning.get", "workspaceId", args.workspaceId);
754
+ const workerId = ClientInput.assertId("worker.reasoning.get", "workerId", args.workerId);
755
+ await this.#assertWorkerOwned(workspaceId, workerId);
756
+ await this.#resolveWorkerModel(workerId, undefined);
757
+ const row = await this.#db.worker_generation_policy_read.get({ id: workerId });
758
+ if (row === undefined)
759
+ throw new Error(`worker ${workerId}: generation policy row missing`);
760
+ if (row.model_route_id === null) {
761
+ if (row.reasoning_policy !== null) {
762
+ throw new Error(`worker ${workerId}: reasoning policy exists without a model route`);
763
+ }
764
+ return { policy: null, supportedPolicies: [] };
765
+ }
766
+ if (row.reasoning_policy === null) {
767
+ throw new Error(`worker ${workerId}: durable model has no reasoning policy`);
768
+ }
769
+ const spec = await specForRoute(this.#db, row.model_route_id);
770
+ if (spec === null)
771
+ throw new Error(`worker ${workerId}: model route is missing`);
772
+ return {
773
+ policy: row.reasoning_policy,
774
+ supportedPolicies: await this.#reasoningSupportForWorker(workerId, row, spec, row.reasoning_policy),
775
+ };
776
+ }
777
+ // {§worker-reasoning-policy} — mutate between loops, validate against both
778
+ // the worker model and its optional spawn model, then persist atomically.
779
+ async setWorkerReasoning(args) {
780
+ const workspaceId = ClientInput.assertId("worker.reasoning.set", "workspaceId", args.workspaceId);
781
+ const workerId = ClientInput.assertId("worker.reasoning.set", "workerId", args.workerId);
782
+ let policy;
783
+ try {
784
+ policy = Validator.assertReasoningPolicy(args.policy);
785
+ }
786
+ catch (cause) {
787
+ throw daemonFailure("daemon:input", "reasoning-policy-invalid", 400, cause instanceof Error ? cause.message : "The reasoning policy is invalid.", { field: "policy", retryable: false });
788
+ }
789
+ await this.#assertWorkerOwned(workspaceId, workerId);
790
+ await this.#assertWorkerSelectable(workerId);
791
+ await this.#resolveWorkerModel(workerId, undefined);
792
+ const row = await this.#db.worker_generation_policy_read.get({ id: workerId });
793
+ if (row === undefined)
794
+ throw new Error(`worker ${workerId}: generation policy row missing`);
795
+ const spec = await specForRoute(this.#db, row.model_route_id);
796
+ if (spec === null) {
797
+ throw daemonFailure("daemon:provider", "worker-model-unset", 409, `Worker ${workerId} has no model against which to validate a reasoning policy.`, {
798
+ workerId,
799
+ stage: "provider-selection",
800
+ recovery: "Select a model before selecting its reasoning policy.",
801
+ retryable: false,
802
+ });
803
+ }
804
+ const supportedPolicies = await this.#reasoningSupportForWorker(workerId, row, spec, policy);
805
+ await this.#db.worker_generation_policy_update.run({
806
+ id: workerId,
807
+ model_route_id: row.model_route_id,
808
+ spawn_model_route_id: row.spawn_model_route_id,
809
+ reasoning_policy: policy,
810
+ });
811
+ return { policy, supportedPolicies };
812
+ }
813
+ // {§worker-model-selection} — persist the worker's spawn override; a null
814
+ // selector means inherit (clears the override).
815
+ async setWorkerSpawnModel(args) {
816
+ const workspaceId = ClientInput.assertId("worker.child.set", "workspaceId", args.workspaceId);
817
+ const workerId = ClientInput.assertId("worker.child.set", "workerId", args.workerId);
818
+ await this.#assertWorkerOwned(workspaceId, workerId);
819
+ await this.#assertWorkerSelectable(workerId);
820
+ const selector = ClientInput.assertChildSelector("worker.child.set", args.selector);
821
+ const spec = await this.#resolveWorkerSpawnModel(workerId, selector);
822
+ return spec === null ? null : projectModelRoute(spec);
823
+ }
824
+ // {§worker-model-selection} — a selection must not mutate underneath active
825
+ // work: a live or parked loop is a precise 409, never a silent retroactive
826
+ // switch of the immutable loop snapshot.
827
+ async #assertWorkerSelectable(workerId) {
828
+ if (await this.#drains.hasLiveWork(workerId)) {
829
+ throw daemonFailure("daemon:worker", "worker-loop-active", 409, `Worker ${workerId} has a live or parked loop; select a model after concluding or cancelling it.`, {
830
+ workerId,
831
+ stage: "model-selection",
832
+ recovery: "Conclude or cancel the active loop before selecting a model.",
833
+ retryable: false,
834
+ });
835
+ }
836
+ }
837
+ async #assertWorkerOwned(workspaceId, workerId) {
838
+ const owner = await this.#db.envelope_get_worker_by_id.get({ id: workerId });
839
+ if (owner === undefined) {
840
+ throw daemonFailure("daemon:worker", "worker-not-found", 404, `Worker ${workerId} does not exist.`, { workerId });
841
+ }
842
+ if (owner.workspace_id !== workspaceId) {
843
+ throw daemonFailure("daemon:worker", "workspace-mismatch", 409, `Worker ${workerId} does not belong to workspace ${workspaceId}.`, { workerId, workspaceId, actualWorkspaceId: owner.workspace_id });
844
+ }
845
+ }
846
+ // {§machine-processes} — provider inference is an exclusive capability of a
847
+ // model worker. The same assertion is used by early client admissions and
848
+ // the final drain callback, so recovery and internal callers cannot bypass it.
849
+ async #assertModelWorker(workspaceId, workerId) {
850
+ const worker = await this.#db.envelope_get_worker_by_id.get({ id: workerId });
851
+ if (worker === undefined) {
852
+ throw daemonFailure("daemon:worker", "worker-not-found", 404, `Worker ${workerId} does not exist.`, { workerId });
853
+ }
854
+ if (worker.workspace_id !== workspaceId) {
855
+ throw daemonFailure("daemon:worker", "workspace-mismatch", 409, `Worker ${workerId} does not belong to workspace ${workspaceId}.`, {
856
+ workerId,
857
+ workspaceId,
858
+ actualWorkspaceId: worker.workspace_id,
859
+ retryable: false,
860
+ });
861
+ }
862
+ if (worker.origin !== "model") {
863
+ throw daemonFailure("daemon:worker", "model-worker-required", 409, `Worker ${workerId} is not a model worker.`, {
864
+ workerId,
865
+ actualOrigin: worker.origin,
866
+ recovery: "Select or create a model worker for this loop.",
867
+ retryable: false,
868
+ });
869
+ }
870
+ }
871
+ async #workerPromptSource(workspaceId, sourceWorkerId, targetWorkerId) {
872
+ if (sourceWorkerId === targetWorkerId)
873
+ return undefined;
874
+ const source = await this.#db.envelope_get_worker_by_id.get({ id: sourceWorkerId });
875
+ if (source === undefined || source.workspace_id !== workspaceId) {
876
+ throw new Error(`Worker prompt source ${sourceWorkerId} does not belong to workspace ${workspaceId}.`);
877
+ }
878
+ return WorkerControlAddress.render(source.name);
481
879
  }
482
- // {§methods-op-mirror} — execute parsed ops on behalf of a client, journaled as a
880
+ // {§methods-op-mirror} — execute parsed ops on behalf of a client as a
483
881
  // client-origin turn (the log is core's, a client op is a first-class citizen), dispatched through
484
882
  // the engine, then emitted as log/entry on the event source. One seam op backs the whole op_*
485
883
  // family (read/edit/copy/find/fold/look/move/open/send/exec); the module parses at its edge with the
486
884
  // grammar package and hands over the statement, then fans the emitted entry out to its own clients.
885
+ // {§functionality-model-mutation} — await every queued Functionality
886
+ // publication (a Worker's accepted mutation publishes at its turn boundary).
887
+ async settleFunctionality(workerId) {
888
+ await this.#functionality.settle(workerId);
889
+ }
487
890
  async dispatchAsClient(args) {
488
891
  const workspaceId = ClientInput.assertId("operation.dispatch", "workspaceId", args.workspaceId);
489
892
  const workerId = ClientInput.assertId("operation.dispatch", "workerId", args.workerId);
893
+ const functionalityWorkerId = ClientInput.assertId("operation.dispatch", "functionalityWorkerId", args.functionalityWorkerId);
490
894
  const { statement } = args;
491
- const clientLoopId = await Envelope.ensureClientLoop(this.#db, workerId);
895
+ await this.#assertWorkerOwned(workspaceId, functionalityWorkerId);
896
+ const releaseCapabilities = await this.#residency.acquire(workspaceId, functionalityWorkerId);
492
897
  try {
493
- const result = await this.#dispatchClientStatement({ workspaceId, workerId, loopId: clientLoopId, statement });
494
- await Envelope.closeClientLoop(this.#db, clientLoopId, { status: 200 });
495
- return result;
898
+ const clientLoopId = await Envelope.ensureClientLoop(this.#db, workerId);
899
+ try {
900
+ const result = await this.#dispatchClientStatement({ workspaceId, workerId, functionalityWorkerId, loopId: clientLoopId, statement });
901
+ await Envelope.closeClientLoop(this.#db, clientLoopId, { status: 200 });
902
+ return result;
903
+ }
904
+ catch (error) {
905
+ await Envelope.closeClientLoop(this.#db, clientLoopId, clientActionFailure(error));
906
+ throw error;
907
+ }
496
908
  }
497
- catch (error) {
498
- await Envelope.closeClientLoop(this.#db, clientLoopId, clientActionFailure(error));
499
- throw error;
909
+ finally {
910
+ releaseCapabilities();
500
911
  }
501
912
  }
502
- // The client-interface action contract: one AG-UI action owns one journal segment,
503
- // regardless of how many statements op.parse produced. A proposed statement may
504
- // keep this promise (and segment) open across interrupt/resume; settlement closes
505
- // it. The journal is durable evidence for the action, not a second client lifecycle.
913
+ // The client-interface action contract: one AG-UI action owns one administrative
914
+ // loop, regardless of how many statements op.parse produced. Each statement is one
915
+ // ordinary operation turn; a proposal may keep that turn and loop open across
916
+ // interrupt/resume until settlement.
506
917
  async dispatchClientAction(args) {
507
918
  const workspaceId = ClientInput.assertId("operation.dispatch-batch", "workspaceId", args.workspaceId);
508
919
  const workerId = ClientInput.assertId("operation.dispatch-batch", "workerId", args.workerId);
920
+ const functionalityWorkerId = ClientInput.assertId("operation.dispatch-batch", "functionalityWorkerId", args.functionalityWorkerId);
509
921
  const { statements } = args;
510
922
  if (statements.length === 0)
511
923
  return [];
512
- const clientLoopId = await Envelope.ensureClientLoop(this.#db, workerId);
924
+ await this.#assertWorkerOwned(workspaceId, functionalityWorkerId);
925
+ // {§actor-boundary-attached-functionality} — the attached Worker's
926
+ // Functionality is what the client operation executes in.
927
+ const releaseCapabilities = await this.#residency.acquire(workspaceId, functionalityWorkerId);
513
928
  try {
514
- const results = [];
515
- for (const statement of statements) {
516
- results.push(await this.#dispatchClientStatement({ workspaceId, workerId, loopId: clientLoopId, statement }));
929
+ const clientLoopId = await Envelope.ensureClientLoop(this.#db, workerId);
930
+ try {
931
+ const results = [];
932
+ for (const statement of statements) {
933
+ results.push(await this.#dispatchClientStatement({ workspaceId, workerId, functionalityWorkerId, loopId: clientLoopId, statement }));
934
+ }
935
+ await Envelope.closeClientLoop(this.#db, clientLoopId, { status: 200 });
936
+ return results;
937
+ }
938
+ catch (error) {
939
+ await Envelope.closeClientLoop(this.#db, clientLoopId, clientActionFailure(error));
940
+ throw error;
517
941
  }
518
- await Envelope.closeClientLoop(this.#db, clientLoopId, { status: 200 });
519
- return results;
520
942
  }
521
- catch (error) {
522
- await Envelope.closeClientLoop(this.#db, clientLoopId, clientActionFailure(error));
523
- throw error;
943
+ finally {
944
+ releaseCapabilities();
524
945
  }
525
946
  }
526
947
  async #dispatchClientStatement(args) {
527
- const { workspaceId, workerId, loopId, statement } = args;
948
+ const { workspaceId, workerId, functionalityWorkerId, loopId, statement } = args;
528
949
  const release = await this.#workspaceGate.acquireTurn(workspaceId, workerId);
529
950
  try {
530
- const { id: turnId } = await JournalTurn.insert(this.#db, loopId);
531
- const entryIds = [];
532
- const result = await this.#engine.dispatch({
533
- statement, workspaceId, workerId, loopId, turnId, sequence: 1,
534
- origin: "client", onDispatch: (logEntryId) => { entryIds.push(logEntryId); },
951
+ const { id: turnId } = await Turn.open(this.#db, {
952
+ loopId,
953
+ producer: "client",
954
+ kind: "operation",
535
955
  });
536
- await this.#branchBatches.sealTurn(turnId);
537
- for (const logEntryId of entryIds) {
538
- const entry = await LogEntry.fetchLogEntry(this.#db, logEntryId);
539
- this.#broadcast({ workspaceId }, "log/entry", { entry });
956
+ let turnOpen = true;
957
+ try {
958
+ const entryIds = [];
959
+ const result = await this.#engine.dispatch({
960
+ statement, workspaceId, workerId, functionalityWorkerId, loopId, turnId, sequence: 1,
961
+ origin: "client", onDispatch: (logEntryId) => { entryIds.push(logEntryId); },
962
+ });
963
+ await Turn.complete(this.#db, turnId, result.status);
964
+ turnOpen = false;
965
+ await this.#branchBatches.sealTurn(turnId);
966
+ for (const logEntryId of entryIds) {
967
+ const entry = await LogEntry.fetchLogEntry(this.#db, logEntryId);
968
+ this.#broadcast({ workspaceId }, "log/entry", { entry });
969
+ }
970
+ return result;
971
+ }
972
+ catch (cause) {
973
+ if (turnOpen) {
974
+ try {
975
+ await Turn.complete(this.#db, turnId, 500);
976
+ }
977
+ catch (completionCause) {
978
+ throw new AggregateError([cause, completionCause], `client operation turn ${turnId} failed and could not be completed`);
979
+ }
980
+ }
981
+ throw cause;
540
982
  }
541
- return result;
542
983
  }
543
984
  finally {
544
985
  release();
@@ -553,20 +994,30 @@ export default class Daemon {
553
994
  async look(args) {
554
995
  const workspaceId = ClientInput.assertId("operation.look", "workspaceId", args.workspaceId);
555
996
  const workerId = ClientInput.assertId("operation.look", "workerId", args.workerId);
997
+ const functionalityWorkerId = ClientInput.assertId("operation.look", "functionalityWorkerId", args.functionalityWorkerId);
556
998
  const { statement } = args;
557
- const release = await this.#workspaceGate.acquireTurn(workspaceId, workerId);
558
- const clientLoopId = await Envelope.ensureClientLoop(this.#db, workerId);
999
+ await this.#assertWorkerOwned(workspaceId, functionalityWorkerId);
1000
+ const releaseCapabilities = await this.#residency.acquire(workspaceId, functionalityWorkerId);
559
1001
  try {
560
- const result = await this.#engine.look({ statement, workspaceId, workerId, loopId: clientLoopId });
561
- await Envelope.closeClientLoop(this.#db, clientLoopId, { status: 200 });
562
- return result;
563
- }
564
- catch (error) {
565
- await Envelope.closeClientLoop(this.#db, clientLoopId, clientActionFailure(error));
566
- throw error;
1002
+ const releaseWorkspace = await this.#workspaceGate.acquireTurn(workspaceId, workerId);
1003
+ try {
1004
+ const clientLoopId = await Envelope.ensureClientLoop(this.#db, workerId);
1005
+ try {
1006
+ const result = await this.#engine.look({ statement, workspaceId, workerId, functionalityWorkerId, loopId: clientLoopId });
1007
+ await Envelope.closeClientLoop(this.#db, clientLoopId, { status: 200 });
1008
+ return result;
1009
+ }
1010
+ catch (error) {
1011
+ await Envelope.closeClientLoop(this.#db, clientLoopId, clientActionFailure(error));
1012
+ throw error;
1013
+ }
1014
+ }
1015
+ finally {
1016
+ releaseWorkspace();
1017
+ }
567
1018
  }
568
1019
  finally {
569
- release();
1020
+ releaseCapabilities();
570
1021
  }
571
1022
  }
572
1023
  // {§methods-log-read} — a workspace's journal, the module's primary render input. The worker is
@@ -630,7 +1081,7 @@ export default class Daemon {
630
1081
  // {§methods} — the module's render surface beyond the journal. Thin delegations
631
1082
  // into core's envelope / membership / provider machinery; the module fans the results into its own views.
632
1083
  listProviders() {
633
- const active = resolveActiveAlias();
1084
+ const active = resolveActiveRoute();
634
1085
  return {
635
1086
  aliases: parseAliasesFromEnv().map((a) => {
636
1087
  const isActive = active !== null && active.alias === a.alias;
@@ -641,6 +1092,11 @@ export default class Daemon {
641
1092
  }),
642
1093
  };
643
1094
  }
1095
+ // {§model-catalog} The catalog is a bounded, worldless local projection.
1096
+ // It performs no provider I/O and never changes the worker's selection.
1097
+ listModels(query) {
1098
+ return listModelCatalog(query);
1099
+ }
644
1100
  // {§client-display-capabilities} Core composes the installed family
645
1101
  // declarations; interface modules expose this contracts-owned wire without
646
1102
  // inventing presentation policy. `exec` is operation machinery, not an
@@ -665,8 +1121,60 @@ export default class Daemon {
665
1121
  return Validator.assertClientDisplayCapabilities([...schemes, ...mimetypes]);
666
1122
  }
667
1123
  listWorkspaces() { return Envelope.listWorkspaces(this.#db); }
668
- listWorkers(workspaceId) {
669
- return Envelope.listWorkersForWorkspace(this.#db, ClientInput.assertId("workspace.workers", "workspaceId", workspaceId));
1124
+ listWorkers(workspaceId, query = {}) {
1125
+ const origin = query.origin;
1126
+ if (origin !== undefined && !["model", "client", "_plurnk"].includes(origin)) {
1127
+ throw daemonFailure("daemon:worker", "origin-invalid", 400, `Worker origin '${String(origin)}' is invalid.`, { origin, retryable: false });
1128
+ }
1129
+ const parentWorkerId = query.parentWorkerId === undefined
1130
+ ? undefined
1131
+ : query.parentWorkerId === null
1132
+ ? null
1133
+ : ClientInput.assertId("workspace.workers", "parentWorkerId", query.parentWorkerId);
1134
+ // {§methods-worker-list} — an omitted parent filter must stay absent:
1135
+ // the envelope keys the filter on the property's presence, not its value.
1136
+ return Envelope.listWorkersForWorkspace(this.#db, ClientInput.assertId("workspace.workers", "workspaceId", workspaceId), {
1137
+ ...(origin === undefined ? {} : { origin }),
1138
+ ...(parentWorkerId === undefined ? {} : { parentWorkerId }),
1139
+ });
1140
+ }
1141
+ async readWorker(args) {
1142
+ const workspaceId = ClientInput.assertId("worker.read", "workspaceId", args.workspaceId);
1143
+ const hasId = Object.hasOwn(args.identity, "id");
1144
+ const hasName = Object.hasOwn(args.identity, "name");
1145
+ if (hasId === hasName) {
1146
+ throw daemonFailure("daemon:worker", "identity-invalid", 400, "Worker observation requires exactly one id or name.", { retryable: false });
1147
+ }
1148
+ const row = hasId
1149
+ ? await this.#db.envelope_get_worker_by_id.get({
1150
+ id: ClientInput.assertId("worker.read", "id", args.identity.id),
1151
+ })
1152
+ : await this.#db.envelope_get_worker_by_name.get({
1153
+ workspace_id: workspaceId,
1154
+ name: ClientInput.assertOptionalWorkerName("worker.read", "name", args.identity.name),
1155
+ });
1156
+ if (row === undefined || row.workspace_id !== workspaceId)
1157
+ return null;
1158
+ const { workspace_id: _workspaceId, ...projection } = row;
1159
+ return projection;
1160
+ }
1161
+ async listWorkerLoops(args) {
1162
+ const workspaceId = ClientInput.assertId("worker.loops", "workspaceId", args.workspaceId);
1163
+ const workerId = ClientInput.assertId("worker.loops", "workerId", args.workerId);
1164
+ await this.#assertWorkerOwned(workspaceId, workerId);
1165
+ const rows = await this.#db.application_list_worker_loops.all({ worker_id: workerId });
1166
+ return rows.map((row) => ({
1167
+ id: row.id,
1168
+ workerId: row.workerId,
1169
+ sequence: row.sequence,
1170
+ status: row.status,
1171
+ prompt: row.prompt,
1172
+ promptSource: row.promptSource,
1173
+ terminatedAt: row.terminatedAt,
1174
+ terminalResult: row.terminalResult === null
1175
+ ? null
1176
+ : Validator.assertOperationResult(JSON.parse(row.terminalResult)),
1177
+ }));
670
1178
  }
671
1179
  // {§methods-workspace-prompts}: root-conversation loop seeds, newest-first.
672
1180
  listPrompts(workspaceId, limit) {
@@ -693,7 +1201,7 @@ export default class Daemon {
693
1201
  }
694
1202
  // {§methods-workspace-create}: the module owns protocol decoding; core validates the typed seam
695
1203
  // inputs and owns the envelope, its reserved-name + name-uniqueness invariants,
696
- // membership resolution, warmWorkspaceDerivations, and the workspace/created emit. No connection state
1204
+ // membership resolution and the workspace/created emit. No connection state
697
1205
  // (which client is on which workspace) lives here — that's the module's.
698
1206
  async createWorkspace(args) {
699
1207
  // The seam fails hard on malformed semantic input so every module inherits one wall:
@@ -713,25 +1221,18 @@ export default class Daemon {
713
1221
  }
714
1222
  if (constraints.length > 0)
715
1223
  await GitMembership.resolveGitMembership(this.#db, envelope.workspaceId, undefined);
716
- for (const provider of this.#workspaceCapabilityProviders.values()) {
717
- await provider.hydrate(envelope.workspaceId);
718
- }
719
- await LoopDocs.materialize(this.#engine, this.#db, envelope.workspaceId);
720
- void this.#engine.warmWorkspaceDerivations(envelope.workspaceId).catch(() => { });
721
1224
  this.#broadcast("all", "workspace/created", { id: envelope.workspaceId, name: envelope.workspaceName, projectRoot: envelope.projectRoot });
722
1225
  return envelope;
723
1226
  });
724
1227
  }
725
1228
  async attachWorkspace(args) {
726
- // attachToWorkspace owns the reserved-name + worker-ownership invariants; the seam just delegates + warms.
1229
+ // attachToWorkspace owns the reserved-name + worker-ownership invariants; the seam just delegates.
727
1230
  const workspaceId = ClientInput.assertId("workspace.attach", "workspaceId", args.workspaceId);
728
1231
  const workerId = args.workerId === undefined
729
1232
  ? undefined
730
1233
  : ClientInput.assertId("workspace.attach", "workerId", args.workerId);
731
1234
  const workerName = ClientInput.assertOptionalWorkerName("workspace.attach", "workerName", args.workerName);
732
- const envelope = await Envelope.attachToWorkspace(this.#db, workspaceId, { workerId, workerName });
733
- void this.#engine.warmWorkspaceDerivations(envelope.workspaceId).catch(() => { });
734
- return envelope;
1235
+ return Envelope.attachToWorkspace(this.#db, workspaceId, { workerId, workerName });
735
1236
  }
736
1237
  async renameWorkspace(workspaceId, name) {
737
1238
  const checkedWorkspaceId = ClientInput.assertId("workspace.rename", "workspaceId", workspaceId);
@@ -750,7 +1251,7 @@ export default class Daemon {
750
1251
  // Members may have just landed — begin warming now, but return the constraint response
751
1252
  // immediately so prompts do not wait for the complete derivation corpus.
752
1253
  void this.#engine.warmWorkspaceDerivations(checkedWorkspaceId).catch(() => { });
753
- return { effect, glob };
1254
+ return { effect, glob, source: "explicit" };
754
1255
  }
755
1256
  finally {
756
1257
  release();
@@ -797,8 +1298,10 @@ export default class Daemon {
797
1298
  retryable: false,
798
1299
  });
799
1300
  }
800
- const release = await this.#workspaceGate.acquireTurn(workspaceId, workerId);
1301
+ const releaseCapabilities = await this.#residency.acquire(workspaceId, workerId);
1302
+ let releaseWorkspace;
801
1303
  try {
1304
+ releaseWorkspace = await this.#workspaceGate.acquireTurn(workspaceId, workerId);
802
1305
  let parsed;
803
1306
  try {
804
1307
  parsed = parsePath(args.target);
@@ -849,6 +1352,7 @@ export default class Daemon {
849
1352
  workspace_id: workspaceId,
850
1353
  owner_id: location.ownerId,
851
1354
  scheme: location.scheme,
1355
+ authority: location.authority,
852
1356
  pathname: location.pathname,
853
1357
  });
854
1358
  if (row === undefined) {
@@ -896,7 +1400,8 @@ export default class Daemon {
896
1400
  });
897
1401
  }
898
1402
  finally {
899
- release();
1403
+ releaseWorkspace?.();
1404
+ releaseCapabilities();
900
1405
  }
901
1406
  }
902
1407
  // {§methods-conversation-worker}: a fresh conversation is a model-origin root worker with an empty private log.
@@ -915,14 +1420,17 @@ export default class Daemon {
915
1420
  }
916
1421
  }
917
1422
  const worker = await Envelope.createModelWorker(this.#db, workspaceId, name);
1423
+ if (args.settings !== undefined) {
1424
+ await this.#mergeWorkerSettings(worker.id, args.settings);
1425
+ }
918
1426
  return { workerId: worker.id, workerName: worker.name };
919
1427
  }
920
1428
  // {§worker-scheme-fork} — branch a worker's log while sharing the workspace world.
921
1429
  // Core owns the workspace check and immutable worker-name admission.
922
1430
  async forkWorker(args) {
923
- const workspaceId = ClientInput.assertId("worker.fork", "workspaceId", args.workspaceId);
924
- const workerId = ClientInput.assertId("worker.fork", "workerId", args.workerId);
925
- const name = ClientInput.assertOptionalWorkerName("worker.fork", "name", args.name);
1431
+ const workspaceId = ClientInput.assertId("run.fork", "workspaceId", args.workspaceId);
1432
+ const workerId = ClientInput.assertId("run.fork", "workerId", args.workerId);
1433
+ const name = ClientInput.assertOptionalWorkerName("run.fork", "name", args.name);
926
1434
  const owner = await this.#db.envelope_get_worker_by_id.get({ id: workerId });
927
1435
  if (owner === undefined) {
928
1436
  throw daemonFailure("daemon:worker", "worker-not-found", 404, `Worker ${workerId} does not exist.`, { workerId });
@@ -941,30 +1449,23 @@ export default class Daemon {
941
1449
  throw daemonFailure("daemon:worker", "name-conflict", 409, `Worker name '${name}' is already in use in workspace ${workspaceId}.`, { workspaceId, name, recovery: "Choose another worker name.", retryable: false });
942
1450
  }
943
1451
  }
944
- const branchWorkerId = await Fork.fork(this.#db, workerId, name);
1452
+ const branchWorkerId = await Fork.fork(this.#db, workerId, name, (scheme) => this.#schemes.entryInheritanceForStoredScheme(scheme, workerId));
945
1453
  const branch = await this.#db.envelope_get_worker_by_id.get({ id: branchWorkerId });
946
1454
  return { workerId: branchWorkerId, workerName: branch?.name ?? null, parentWorkerId: workerId };
947
1455
  }
948
1456
  async registerRuntimes(registrations) {
949
1457
  const normalized = registrations.map((registration) => this.#normalizeRuntime(registration));
950
1458
  this.#engine.registerRuntimes(normalized);
951
- if (this.#capabilitiesPublished) {
952
- for (const workspace of await Envelope.listWorkspaces(this.#db)) {
953
- await LoopDocs.materialize(this.#engine, this.#db, workspace.id);
954
- }
955
- }
1459
+ await this.#residency.rematerializeActive();
956
1460
  }
957
1461
  async registerRuntime(registration) {
958
1462
  await this.registerRuntimes([registration]);
959
1463
  }
960
1464
  async registerScheme(name, handler) {
961
1465
  this.#schemes.register(name, handler);
962
- if (this.#capabilitiesPublished) {
1466
+ if (this.#residency.published)
963
1467
  await this.#schemes.ready();
964
- for (const workspace of await Envelope.listWorkspaces(this.#db)) {
965
- await LoopDocs.materialize(this.#engine, this.#db, workspace.id);
966
- }
967
- }
1468
+ await this.#residency.rematerializeActive();
968
1469
  }
969
1470
  #normalizeRuntime({ namespaceOwner, decl, executor, availability, scheme }) {
970
1471
  if (typeof namespaceOwner !== "string" || namespaceOwner.trim().length === 0) {
@@ -977,8 +1478,11 @@ export default class Daemon {
977
1478
  executor,
978
1479
  namespaceOwner: { kind: "module", name: namespaceOwner },
979
1480
  glyph: runtime.glyph ?? "",
1481
+ summary: runtime.summary,
980
1482
  invocation: runtime.invocation,
981
- documentation: runtime.documentation ?? "",
1483
+ details: runtime.details ?? "",
1484
+ ...(runtime.resourcesPath === undefined ? {} : { resourcesPath: runtime.resourcesPath }),
1485
+ ...(runtime.expandTools === undefined ? {} : { expandTools: runtime.expandTools }),
982
1486
  available: availability.available,
983
1487
  detail: availability.detail,
984
1488
  },
@@ -986,12 +1490,26 @@ export default class Daemon {
986
1490
  };
987
1491
  }
988
1492
  registerModuleAction(registration) {
989
- const { name, scope, handler } = registration;
1493
+ const { name, scope, inputSchema, outputSchema, handler } = registration;
990
1494
  if (name.length === 0)
991
1495
  throw new Error("registerModuleAction: action name must not be empty");
992
- if (scope !== "worldless" && scope !== "workspace") {
1496
+ if (scope !== "worldless" && scope !== "workspace" && scope !== "worker") {
993
1497
  throw new Error(`module action '${name}' has invalid scope '${String(scope)}'`);
994
1498
  }
1499
+ if (inputSchema === null || typeof inputSchema !== "object" || Array.isArray(inputSchema)) {
1500
+ throw new Error(`module action '${name}' has no input schema`);
1501
+ }
1502
+ if (outputSchema === null || typeof outputSchema !== "object" || Array.isArray(outputSchema)) {
1503
+ throw new Error(`module action '${name}' has no output schema`);
1504
+ }
1505
+ for (const [role, schema] of [["input", inputSchema], ["output", outputSchema]]) {
1506
+ try {
1507
+ Validator.validateJsonSchemaInstance(schema, {});
1508
+ }
1509
+ catch (cause) {
1510
+ throw new Error(`module action '${name}' has an invalid ${role} schema`, { cause });
1511
+ }
1512
+ }
995
1513
  if (typeof handler !== "function")
996
1514
  throw new Error(`module action '${name}' has no handler`);
997
1515
  if (this.#moduleActions.has(name))
@@ -1000,7 +1518,12 @@ export default class Daemon {
1000
1518
  }
1001
1519
  listModuleActions() {
1002
1520
  return [...this.#moduleActions.values()]
1003
- .map(({ name, scope }) => ({ name, scope }))
1521
+ .map(({ name, scope, inputSchema, outputSchema }) => ({
1522
+ name,
1523
+ scope,
1524
+ inputSchema,
1525
+ outputSchema,
1526
+ }))
1004
1527
  .toSorted((left, right) => left.name.localeCompare(right.name));
1005
1528
  }
1006
1529
  async invokeModuleAction(name, params, context) {
@@ -1010,129 +1533,38 @@ export default class Daemon {
1010
1533
  if (registration.scope !== context.scope) {
1011
1534
  throw new Error(`module action '${name}' requires ${registration.scope} context, not ${context.scope}`);
1012
1535
  }
1536
+ let releaseCapabilities;
1013
1537
  if (context.scope === "workspace") {
1014
1538
  ClientInput.assertId(`module action '${name}'`, "workspaceId", context.workspaceId);
1015
1539
  }
1016
- return registration.handler(params, context);
1017
- }
1018
- registerWorkspaceCapabilityProvider(namespaceOwner, provider) {
1019
- if (namespaceOwner.trim().length === 0) {
1020
- throw new Error("workspace capability provider requires a non-empty namespace owner");
1021
- }
1022
- if (this.#workspaceCapabilityProviders.has(namespaceOwner)) {
1023
- throw new Error(`workspace capability provider '${namespaceOwner}' is already registered`);
1024
- }
1025
- this.#workspaceCapabilityProviders.set(namespaceOwner, provider);
1026
- }
1027
- async readWorkspaceModuleState(workspaceId, namespaceOwner) {
1028
- const checkedWorkspaceId = ClientInput.assertId("workspace module state", "workspaceId", workspaceId);
1029
- if (namespaceOwner.trim().length === 0) {
1030
- throw new Error("workspace module state requires a non-empty namespace owner");
1031
- }
1032
- const row = await this.#db.workspace_module_state_get.get({
1033
- workspace_id: checkedWorkspaceId,
1034
- namespace_owner: namespaceOwner,
1035
- });
1036
- return row === undefined ? null : JSON.parse(row.state);
1037
- }
1038
- async replaceWorkspaceCapabilities({ workspaceId, namespaceOwner, state, runtimes, }) {
1039
- const checkedWorkspaceId = ClientInput.assertId("workspace capability replacement", "workspaceId", workspaceId);
1040
- if (namespaceOwner.trim().length === 0) {
1041
- throw new Error("workspace capability replacement requires a non-empty namespace owner");
1042
- }
1043
- const workspace = await this.#db.envelope_get_workspace.get({ id: checkedWorkspaceId });
1044
- if (workspace === undefined) {
1045
- throw daemonFailure("daemon:workspace-capability", "workspace-not-found", 404, `Workspace ${checkedWorkspaceId} does not exist.`, { workspaceId: checkedWorkspaceId, retryable: false });
1046
- }
1047
- const encoded = state === null ? null : JSON.stringify(state);
1048
- if (state !== null && encoded === undefined) {
1049
- throw daemonFailure("daemon:workspace-capability", "state-not-json", 400, "Workspace module state is not JSON-serializable.", { namespaceOwner, retryable: false });
1050
- }
1051
- if (encoded !== null)
1052
- JSON.parse(encoded);
1053
- const normalized = runtimes.map((registration) => {
1054
- if (registration.namespaceOwner !== namespaceOwner) {
1055
- throw new Error(`workspace runtime owner '${registration.namespaceOwner}' does not match '${namespaceOwner}'`);
1540
+ else if (context.scope === "worker") {
1541
+ const workspaceId = ClientInput.assertId(`module action '${name}'`, "workspaceId", context.workspaceId);
1542
+ const workerId = ClientInput.assertId(`module action '${name}'`, "workerId", context.workerId);
1543
+ const worker = await this.#db.envelope_get_worker_by_id.get({ id: workerId });
1544
+ if (worker === undefined || worker.workspace_id !== workspaceId) {
1545
+ throw daemonFailure("daemon:module-action", "worker-not-found", 404, `Worker ${workerId} does not exist in workspace ${workspaceId}.`, { workspaceId, workerId, retryable: false });
1056
1546
  }
1057
- return this.#normalizeRuntime(registration);
1058
- });
1059
- const gate = this.#workspaceGate.tryExclusive(checkedWorkspaceId);
1060
- if (gate === null) {
1061
- throw daemonFailure("daemon:workspace-capability", "workspace-busy", 409, `Workspace ${checkedWorkspaceId} is running an operation or another capability change.`, {
1062
- workspaceId: checkedWorkspaceId,
1063
- namespaceOwner,
1064
- recovery: "Settle the current operation and retry the capability change.",
1065
- retryable: true,
1066
- });
1547
+ releaseCapabilities = await this.#residency.acquire(workspaceId, workerId);
1067
1548
  }
1068
- await gate.acquired;
1069
- const prior = await this.#db.workspace_module_state_get.get({
1070
- workspace_id: checkedWorkspaceId,
1071
- namespace_owner: namespaceOwner,
1072
- });
1073
- let rollbackRuntimes;
1074
- let stateChanged = false;
1075
1549
  try {
1076
- const commitRuntimes = await this.#engine.prepareWorkspaceRuntimes(checkedWorkspaceId, namespaceOwner, normalized);
1077
- if (encoded === null) {
1078
- await this.#db.workspace_module_state_delete.run({
1079
- workspace_id: checkedWorkspaceId,
1080
- namespace_owner: namespaceOwner,
1081
- });
1082
- }
1083
- else {
1084
- await this.#db.workspace_module_state_put.run({
1085
- workspace_id: checkedWorkspaceId,
1086
- namespace_owner: namespaceOwner,
1087
- state: encoded,
1088
- });
1089
- }
1090
- stateChanged = true;
1091
- rollbackRuntimes = commitRuntimes();
1092
- if (this.#capabilitiesPublished) {
1093
- await LoopDocs.materialize(this.#engine, this.#db, checkedWorkspaceId);
1094
- }
1095
- }
1096
- catch (cause) {
1097
- rollbackRuntimes?.();
1098
- const rollbackErrors = [];
1099
- if (stateChanged) {
1100
- try {
1101
- if (prior === undefined) {
1102
- await this.#db.workspace_module_state_delete.run({
1103
- workspace_id: checkedWorkspaceId,
1104
- namespace_owner: namespaceOwner,
1105
- });
1106
- }
1107
- else {
1108
- await this.#db.workspace_module_state_put.run({
1109
- workspace_id: checkedWorkspaceId,
1110
- namespace_owner: namespaceOwner,
1111
- state: prior.state,
1112
- });
1113
- }
1114
- }
1115
- catch (rollbackCause) {
1116
- rollbackErrors.push(rollbackCause);
1117
- }
1118
- if (this.#capabilitiesPublished) {
1119
- try {
1120
- await LoopDocs.materialize(this.#engine, this.#db, checkedWorkspaceId);
1121
- }
1122
- catch (rollbackCause) {
1123
- rollbackErrors.push(rollbackCause);
1124
- }
1125
- }
1126
- }
1127
- if (rollbackErrors.length > 0) {
1128
- throw new AggregateError([cause, ...rollbackErrors], "Workspace capability replacement and rollback failed");
1129
- }
1130
- throw cause;
1550
+ return await registration.handler(params, context);
1131
1551
  }
1132
1552
  finally {
1133
- gate.release();
1553
+ releaseCapabilities?.();
1134
1554
  }
1135
1555
  }
1556
+ registerWorkerCapabilityProvider(namespaceOwner, provider) {
1557
+ this.#residency.registerProvider(namespaceOwner, provider);
1558
+ }
1559
+ registerFunctionalityAdapter(adapter) {
1560
+ return this.#functionality.register(adapter);
1561
+ }
1562
+ async readWorkerModuleState(workerId, namespaceOwner) {
1563
+ return this.#residency.readModuleState(workerId, namespaceOwner);
1564
+ }
1565
+ async replaceWorkerCapabilities(replacement, options = {}) {
1566
+ await this.#residency.replace(replacement, options);
1567
+ }
1136
1568
  get engine() { return this.#engine; }
1137
1569
  get provider() { return this.#provider; }
1138
1570
  get schemes() { return this.#schemes; }
@@ -1158,6 +1590,24 @@ export default class Daemon {
1158
1590
  // shell is the default runtime, so its executor must boot usable.
1159
1591
  const executors = await ExecutorRegistry.build({ defaultRuntime: "sh", cwd: this.#discoveryCwd });
1160
1592
  this.#engine.setExecutors(executors);
1593
+ this.#engine.setFunctionalityDocuments((workerId) => this.#functionality.documents(workerId));
1594
+ // {§question-tool} — the native request-user-input runtime, process-wide;
1595
+ // per-worker admission gates its doc visibility and dispatch ({§worker-settings}).
1596
+ await this.#engine.registerRuntimes([{
1597
+ tag: "question",
1598
+ entry: {
1599
+ executor: new QuestionTool({ runtime: "question", glyph: "❓" }),
1600
+ namespaceOwner: { kind: "module", name: "core" },
1601
+ glyph: "❓",
1602
+ summary: questionRuntimeDecl.summary,
1603
+ invocation: questionRuntimeDecl.invocation,
1604
+ details: questionRuntimeDecl.details ?? "",
1605
+ available: true,
1606
+ detail: "in-process",
1607
+ },
1608
+ }]);
1609
+ // {§effect-policy-tunable} — invalid operator policy fails boot, not the first EXEC.
1610
+ EffectPolicy.validateConfiguration();
1161
1611
  // {§exec} — mint a scheme per runtime tag so exec output entries address by tag
1162
1612
  // authority (sh:///l/t/s). The "exec" scheme stays for the EXEC op dispatch.
1163
1613
  this.#schemes.registerRuntimeSchemes(executors);
@@ -1165,25 +1615,24 @@ export default class Daemon {
1165
1615
  // (agnostic, by plurnk.kind:"scheme"). They light up http://, etc. with
1166
1616
  // no further engine change — #run wraps their context in SchemeCtxImpl ({§plugin-discovery}).
1167
1617
  await this.#schemes.discoverExternal(this.#discoveryCwd);
1618
+ // {§module-discovery} — third-party daemon-module composition: trusted
1619
+ // packages declaring `plurnk.kind: "module"` register beside the
1620
+ // service's explicit composition before any module setup runs.
1621
+ const discoveredModules = await discoverDaemonModules({ cwd: this.#discoveryCwd });
1622
+ for (const packageName of discoveredModules.skipped) {
1623
+ console.warn(`module discovery: '${packageName}' is discovered but untrusted (PLURNK_PLUGINS_TRUSTED_ONLY); not registered`);
1624
+ }
1625
+ for (const module of discoveredModules.modules) {
1626
+ this.#modules.push(module);
1627
+ }
1168
1628
  const setupSeam = this;
1169
1629
  for (const module of this.#modules) {
1170
1630
  if (module.close !== undefined)
1171
1631
  this.#moduleClosers.push(module);
1172
1632
  await module.setup?.(setupSeam);
1173
1633
  }
1174
- for (const workspace of await Envelope.listWorkspaces(this.#db)) {
1175
- for (const provider of this.#workspaceCapabilityProviders.values()) {
1176
- await provider.hydrate(workspace.id);
1177
- }
1178
- }
1179
1634
  await this.#schemes.ready();
1180
- // Reconcile the kernel-published documentation surface once per existing workspace.
1181
- // Installed capabilities and operator configuration are now fully known; model loops
1182
- // consume this workspace state but never republish it.
1183
- for (const workspace of await Envelope.listWorkspaces(this.#db)) {
1184
- await LoopDocs.materialize(this.#engine, this.#db, workspace.id);
1185
- }
1186
- this.#capabilitiesPublished = true;
1635
+ this.#residency.publish();
1187
1636
  await this.#recoverLifecycle();
1188
1637
  // {§module-lifecycle} — the daemon opens no transport. Modules start their listeners only
1189
1638
  // after capability publication and durable lifecycle recovery are complete.
@@ -1198,6 +1647,7 @@ export default class Daemon {
1198
1647
  await this.#db.recovery_fail_active_loops.run({});
1199
1648
  await this.#db.recovery_settle_open_provider_requests.run({});
1200
1649
  await this.#db.recovery_fail_open_model_calls.run({});
1650
+ await this.#db.recovery_fail_open_turns.run({});
1201
1651
  await this.#db.recovery_fail_ownerless_proposals.run({});
1202
1652
  await this.#db.recovery_remove_ownerless_client_interactions.run({});
1203
1653
  await this.#db.recovery_error_orphan_subscription_channels.run({});
@@ -1205,11 +1655,24 @@ export default class Daemon {
1205
1655
  await this.#db.recovery_resume_unblocked_parks.run({});
1206
1656
  await this.#branchBatches.recover();
1207
1657
  const orphanSources = await this.#db.recovery_orphan_prompt_sources.all({});
1658
+ const invalidOrphan = orphanSources.find(({ origin }) => origin !== "model");
1659
+ if (invalidOrphan !== undefined) {
1660
+ throw new Error(`Orphaned provider work belongs to ${invalidOrphan.origin} worker ${invalidOrphan.worker_id}; only model workers may enter inference.`);
1661
+ }
1208
1662
  for (const source of orphanSources) {
1209
1663
  await this.#drains.reconcileOrphanedPrompts(source.worker_id, source.loop_id);
1210
1664
  }
1211
1665
  const systemPrompt = await readFile(Paths.instructionsSystem, "utf8");
1212
1666
  const queued = await this.#db.recovery_queued_workers.all({});
1667
+ const invalidQueued = queued.find(({ origin }) => origin !== "model");
1668
+ if (invalidQueued !== undefined) {
1669
+ throw new Error(`Queued provider work belongs to ${invalidQueued.origin} worker ${invalidQueued.worker_id}; only model workers may enter inference.`);
1670
+ }
1671
+ const parked = await this.#db.recovery_parked_workers.all({});
1672
+ const invalidParked = parked.find(({ origin }) => origin !== "model");
1673
+ if (invalidParked !== undefined) {
1674
+ throw new Error(`Parked provider work belongs to ${invalidParked.origin} worker ${invalidParked.worker_id}; only model workers may enter inference.`);
1675
+ }
1213
1676
  for (const row of queued) {
1214
1677
  const started = await this.#drains.ensureDrain({
1215
1678
  workspaceId: row.workspace_id,
@@ -1217,18 +1680,52 @@ export default class Daemon {
1217
1680
  systemPrompt,
1218
1681
  });
1219
1682
  started?.drainPromise.catch((err) => {
1220
- console.error(`recovered drain failed for worker ${row.worker_id}:`, err);
1683
+ if (this.#started) {
1684
+ console.error(`recovered drain failed for worker ${row.worker_id}:`, err);
1685
+ }
1221
1686
  });
1222
1687
  }
1223
- const parked = await this.#db.recovery_parked_workers.all({});
1224
1688
  for (const row of parked) {
1225
1689
  await this.#drains.schedulePollWake(row.workspace_id, row.worker_id, systemPrompt);
1226
1690
  }
1227
1691
  }
1692
+ // {§crash-only-stop} — the settle deadline. Default 30s; an operator can
1693
+ // raise it for slow hosts or lower it for tests. 0 is refused (an unbounded
1694
+ // stop is exactly the wedge this exists to prevent).
1695
+ static #stopDeadlineMs() {
1696
+ const raw = Number(process.env.PLURNK_SERVICE_STOP_TIMEOUT_MS ?? 30_000);
1697
+ if (!Number.isSafeInteger(raw) || raw <= 0) {
1698
+ throw new Error(`PLURNK_SERVICE_STOP_TIMEOUT_MS must be a positive integer; got ${JSON.stringify(process.env.PLURNK_SERVICE_STOP_TIMEOUT_MS)}.`);
1699
+ }
1700
+ return raw;
1701
+ }
1228
1702
  async stop() {
1229
1703
  if (!this.#started)
1230
1704
  return;
1231
1705
  this.#started = false;
1706
+ // Stop every producer before refusing capability acquisition. A drain
1707
+ // already between queue claim and activation must observe its own abort,
1708
+ // not misclassify orderly shutdown as a capability failure.
1709
+ const derivationAbort = new DOMException("daemon stopping", "AbortError");
1710
+ this.#engine.cancelAllProposals("daemon_stopping");
1711
+ this.#engine.cancelDerivations(derivationAbort);
1712
+ this.#branchBatches.beginStop();
1713
+ this.#drains.beginStop("daemon_stopping");
1714
+ this.#residency.beginStop();
1715
+ const stopDeadlineMs = Daemon.#stopDeadlineMs();
1716
+ const deadline = Date.now() + stopDeadlineMs;
1717
+ const settle = (label, wait) => new Promise((resolve) => {
1718
+ let settled = false;
1719
+ const finish = (result) => {
1720
+ if (settled)
1721
+ return;
1722
+ settled = true;
1723
+ clearTimeout(timer);
1724
+ resolve(result);
1725
+ };
1726
+ const timer = setTimeout(() => finish({ status: "rejected", reason: new Error(`stop deadline exceeded waiting for ${label}`) }), Math.max(0, deadline - Date.now()));
1727
+ Promise.resolve().then(wait).then((value) => finish({ status: "fulfilled", value }), (reason) => finish({ status: "rejected", reason }));
1728
+ });
1232
1729
  // Stop accepting external work immediately, but do not await listener
1233
1730
  // closure before cancelling active workers: an SSE connection may itself be
1234
1731
  // waiting for the worker cancellation that follows.
@@ -1248,32 +1745,39 @@ export default class Daemon {
1248
1745
  // Settle the stopped world FIRST: a drain paused at a pending proposal awaits a resolution
1249
1746
  // that will never arrive once clients are gone — allSettled(drains) below would deadlock
1250
1747
  // the stop forever (a daemon with a pending HITL proposal could not shut down).
1251
- const derivationAbort = new DOMException("daemon stopping", "AbortError");
1252
- this.#engine.cancelAllProposals("daemon_stopping");
1253
- this.#engine.cancelDerivations(derivationAbort);
1254
- this.#branchBatches.beginStop();
1255
- this.#drains.beginStop("daemon_stopping");
1256
- await this.#branchBatches.idle();
1257
- const [drainResult] = await Promise.allSettled([this.#drains.idle()]);
1258
- const closeResults = await moduleClose;
1259
- const [streamingResult] = await Promise.allSettled([this.#drainStreamingSchemes()]);
1260
- const [derivationResult] = await Promise.allSettled([
1261
- this.#engine.drainDerivations(derivationAbort), // active workspace warms settle before the db closes upstream
1262
- ]);
1263
- const mimetypeResults = this.#ownsMimetypes
1264
- ? await Promise.allSettled([this.#mimetypes.dispose()])
1265
- : [];
1266
- const [schemeResult] = await Promise.allSettled([this.#schemes.close()]);
1748
+ // {§crash-only-stop} — the settle sequence is DEADLINE-BOUNDED: a child
1749
+ // that never closes (a wedged MCP server, a stuck stream) must not hang
1750
+ // the daemon forever. Past the deadline the waits are abandoned; the
1751
+ // process may exit with the WAL in place (SQLite recovers) rather than
1752
+ // leak as a live-but-wedged tree.
1753
+ const branchResult = await settle("branch batches idle", () => this.#branchBatches.idle());
1754
+ const drainResult = await settle("drains idle", () => this.#drains.idle());
1755
+ // A boundary publication queued behind the last turn settles before
1756
+ // modules close and before the DB goes away ({§functionality-publication}).
1757
+ const functionalityResult = await settle("functionality publications", () => this.#functionality.settle());
1758
+ const moduleResult = await settle("modules close", async () => {
1759
+ const results = await moduleClose;
1760
+ if (results.some((r) => r.status === "rejected"))
1761
+ throw new AggregateError(results.filter((r) => r.status === "rejected").map((r) => r.reason), "module close failed");
1762
+ });
1763
+ const streamingResult = await settle("streaming schemes idle", () => this.#drainStreamingSchemes());
1764
+ const derivationResult = await settle("derivation drain", () => this.#engine.drainDerivations(derivationAbort));
1765
+ const mimetypeResult = this.#ownsMimetypes
1766
+ ? await settle("mimetypes dispose", () => this.#mimetypes.dispose())
1767
+ : null;
1768
+ const schemeResult = await settle("schemes close", () => this.#schemes.close());
1267
1769
  // Streaming and scheme closure are the last producers of synchronous
1268
1770
  // conclusion notifications. Join the supervisor-owned async tails only
1269
1771
  // after those producers settle, before the caller may close SQLite.
1270
- const [wakeResult] = await Promise.allSettled([this.#drains.idle()]);
1772
+ const wakeResult = await settle("drains idle (wake)", () => this.#drains.idle());
1271
1773
  const closeErrors = [
1272
- ...closeResults,
1774
+ moduleResult,
1775
+ functionalityResult,
1776
+ branchResult,
1273
1777
  drainResult,
1274
1778
  streamingResult,
1275
1779
  derivationResult,
1276
- ...mimetypeResults,
1780
+ ...(mimetypeResult === null ? [] : [mimetypeResult]),
1277
1781
  schemeResult,
1278
1782
  wakeResult,
1279
1783
  ]
@@ -1299,6 +1803,10 @@ export default class Daemon {
1299
1803
  notifyStreamEvent(workspaceId, event) {
1300
1804
  this.#broadcast({ workspaceId }, "stream/event", event);
1301
1805
  }
1806
+ /** Emit transient readable reasoning for an in-flight model call. */
1807
+ notifyReasoningEvent(workspaceId, event) {
1808
+ this.#broadcast({ workspaceId }, "reasoning/event", event);
1809
+ }
1302
1810
  /**
1303
1811
  * Emit a transient notice scoped to the workspace containing the loop.
1304
1812
  */
@@ -1344,9 +1852,11 @@ export default class Daemon {
1344
1852
  worker_id: workerId,
1345
1853
  sequence: seqRow.next,
1346
1854
  prompt: first.body,
1855
+ prompt_source: first.prompt_source,
1347
1856
  flags: first.flags,
1348
- provider_spec: first.provider_spec,
1349
- child_provider_spec: first.child_provider_spec,
1857
+ model_route_id: first.model_route_id,
1858
+ spawn_model_route_id: first.spawn_model_route_id,
1859
+ reasoning_policy: first.reasoning_policy,
1350
1860
  max_turns: first.max_turns,
1351
1861
  open_paths: first.open_paths ?? "[]",
1352
1862
  orphan_source_loop_id: endedLoopId,
@@ -1369,6 +1879,12 @@ export default class Daemon {
1369
1879
  cancelDrain(workerId, reason = "user_cancelled") {
1370
1880
  return this.#drains.cancel(workerId, reason);
1371
1881
  }
1882
+ async cancelWorker(args) {
1883
+ const workspaceId = ClientInput.assertId("worker.cancel", "workspaceId", args.workspaceId);
1884
+ const workerId = ClientInput.assertId("worker.cancel", "workerId", args.workerId);
1885
+ await this.#assertWorkerOwned(workspaceId, workerId);
1886
+ await this.#drains.cancelWorkerTree(workerId, args.reason ?? "user_cancelled");
1887
+ }
1372
1888
  // Process-local stream activity is supplied to the drain owner through the
1373
1889
  // scheme registry rather than exposing that registry across the boundary.
1374
1890
  #workerHasActiveStreams(workerId) {