@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
@@ -19,17 +19,21 @@ CREATE TABLE IF NOT EXISTS workspaces (
19
19
 
20
20
  CREATE INDEX IF NOT EXISTS workspaces_created_at ON workspaces (created_at);
21
21
 
22
- -- {§module-workspace-state}: one opaque, provider-validated JSON snapshot per
23
- -- module owner and workspace. Core owns isolation and lifecycle only.
24
- CREATE TABLE IF NOT EXISTS workspace_module_state (
25
- workspace_id INTEGER NOT NULL,
26
- namespace_owner TEXT NOT NULL CHECK (length(namespace_owner) > 0),
27
- state TEXT NOT NULL CHECK (json_valid(state)),
28
- updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
29
- PRIMARY KEY (workspace_id, namespace_owner),
30
- FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE
22
+ -- model_routes — the immutable resolved model route ({§worker-model-selection}). One row per
23
+ -- complete resolved tuple; append-only. Alias is provenance plus a tuning scope,
24
+ -- not route identity, and is absent on a direct provider/model selection.
25
+ CREATE TABLE IF NOT EXISTS model_routes (
26
+ id INTEGER NOT NULL PRIMARY KEY,
27
+ alias TEXT CHECK (alias IS NULL OR length(alias) > 0),
28
+ provider TEXT NOT NULL CHECK (length(provider) > 0),
29
+ model TEXT NOT NULL CHECK (length(model) > 0),
30
+ base_url TEXT CHECK (base_url IS NULL OR length(base_url) > 0),
31
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
31
32
  ) STRICT;
32
33
 
34
+ CREATE UNIQUE INDEX IF NOT EXISTS model_routes_identity
35
+ ON model_routes (coalesce(alias, ''), provider, model, coalesce(base_url, ''));
36
+
33
37
  -- workers
34
38
  CREATE TABLE IF NOT EXISTS workers (
35
39
  id INTEGER NOT NULL PRIMARY KEY,
@@ -41,16 +45,34 @@ CREATE TABLE IF NOT EXISTS workers (
41
45
  -- independent databases reuse local integer ids.
42
46
  provider_identity TEXT NOT NULL DEFAULT (lower(hex(randomblob(16))))
43
47
  CHECK (length(provider_identity) = 32 AND provider_identity NOT GLOB '*[^0-9a-f]*'),
48
+ -- {§worker-model-selection}: the worker's durable resolved model, NULL for non-model
49
+ -- workers or a deliberately modelless unresolved worker. spawn_model_route_id is the
50
+ -- persistent spawn override; NULL means "use my model."
51
+ model_route_id INTEGER REFERENCES model_routes(id),
52
+ spawn_model_route_id INTEGER REFERENCES model_routes(id),
53
+ -- {§worker-reasoning-policy}: nullable only while the worker has no model;
54
+ -- once selected, model and reasoning policy form one durable generation policy.
55
+ reasoning_policy TEXT CHECK (reasoning_policy IS NULL OR reasoning_policy IN ('off', 'adaptive', 'low', 'medium', 'high')),
44
56
  -- workers fork via parent_worker_id; workspaces carry no parent — {§machine-processes-no-fork-workspace}
45
57
  parent_worker_id INTEGER CHECK (parent_worker_id IS NULL OR parent_worker_id != id),
46
- origin TEXT NOT NULL DEFAULT 'client' CHECK (origin IN ('model', 'client', 'plurnk')),
58
+ origin TEXT NOT NULL DEFAULT 'client' CHECK (origin IN ('model', 'client', '_plurnk')),
47
59
  -- {§methods-model-worker}: durable identity for the workspace's stable
48
60
  -- default conversation; unrelated to its human-facing, reclaimable name.
49
61
  default_conversation INTEGER NOT NULL DEFAULT 0 CHECK (default_conversation IN (0, 1)),
62
+ -- {§worker-settings}: the worker's own behavioral rules inside the workspace's
63
+ -- world — the workspace is how things are; each worker carries the rules its
64
+ -- loops obey. Client-declared at worker creation, mutable between loops,
65
+ -- validated at the client-input boundary (closed known-key set; unknown keys
66
+ -- never persist).
67
+ settings TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(settings)),
50
68
  -- {§env-delta-log-pull}: monotonic observation progress, not a private world snapshot.
51
- -- NULL means the worker has not established its first-turn baseline yet.
69
+ -- Creation captures the workspace high-water; a fork instead copies its
70
+ -- parent's cursor and records the closed event boundary of its snapshot.
52
71
  ambient_event_cursor INTEGER CHECK (ambient_event_cursor IS NULL OR ambient_event_cursor >= 0),
72
+ fork_event_boundary INTEGER CHECK (fork_event_boundary IS NULL OR fork_event_boundary >= 0),
73
+ CHECK (fork_event_boundary IS NULL OR parent_worker_id IS NOT NULL),
53
74
  CHECK (default_conversation = 0 OR (origin = 'model' AND parent_worker_id IS NULL)),
75
+ CHECK ((model_route_id IS NULL) = (reasoning_policy IS NULL)),
54
76
  FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE,
55
77
  FOREIGN KEY (parent_worker_id) REFERENCES workers(id) ON DELETE CASCADE
56
78
  ) STRICT;
@@ -73,48 +95,133 @@ BEGIN
73
95
  SELECT RAISE(ABORT, 'workers.provider_identity is immutable');
74
96
  END;
75
97
 
76
- -- {§env-delta-log-pull}: one append-only occurrence journal gives every ambient
77
- -- producer a shared monotonic order. It snapshots only what the observer row
78
- -- needs; shared-world contents remain owned by entries/files, never copied here.
98
+ CREATE TRIGGER IF NOT EXISTS workers_fork_event_boundary_immutable
99
+ BEFORE UPDATE OF fork_event_boundary ON workers
100
+ WHEN NEW.fork_event_boundary IS NOT OLD.fork_event_boundary
101
+ BEGIN
102
+ SELECT RAISE(ABORT, 'workers.fork_event_boundary is immutable');
103
+ END;
104
+
105
+ -- {§module-worker-state}: one opaque, provider-validated JSON snapshot per
106
+ -- module owner and Worker. Core owns isolation, inheritance, and lifecycle;
107
+ -- the module owns its state schema and semantics.
108
+ CREATE TABLE IF NOT EXISTS worker_module_state (
109
+ worker_id INTEGER NOT NULL,
110
+ namespace_owner TEXT NOT NULL CHECK (length(namespace_owner) > 0),
111
+ state TEXT NOT NULL CHECK (json_valid(state)),
112
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
113
+ PRIMARY KEY (worker_id, namespace_owner),
114
+ FOREIGN KEY (worker_id) REFERENCES workers(id) ON DELETE CASCADE
115
+ ) STRICT;
116
+
117
+ -- A child snapshots every initialized module state at birth. Later parent
118
+ -- mutations do not propagate; the child and parent are independent authorities.
119
+ CREATE TRIGGER IF NOT EXISTS worker_module_state_inherit
120
+ AFTER INSERT ON workers
121
+ WHEN NEW.parent_worker_id IS NOT NULL
122
+ BEGIN
123
+ INSERT INTO worker_module_state (worker_id, namespace_owner, state, updated_at)
124
+ SELECT NEW.id, namespace_owner, state, updated_at
125
+ FROM worker_module_state
126
+ WHERE worker_id = NEW.parent_worker_id;
127
+ END;
128
+
129
+ -- {§env-delta-log-pull}: one append-only occurrence journal gives every
130
+ -- producer a shared monotonic order. Audience is structural: direct parent,
131
+ -- explicit workspace broadcast, or their union. The event snapshots exactly
132
+ -- what an observer row needs because source-log curation cannot erase history.
79
133
  -- source_record_id is forensic identity for the originating log/loop row, not a
80
134
  -- foreign key: model log curation must not erase an already-recorded occurrence.
81
135
  CREATE TABLE IF NOT EXISTS ambient_events (
82
- id INTEGER NOT NULL PRIMARY KEY,
83
- workspace_id INTEGER NOT NULL,
84
- producer_worker_id INTEGER NOT NULL,
85
- kind TEXT NOT NULL CHECK (kind IN ('edit', 'loop_termination')),
86
- source_record_id INTEGER NOT NULL CHECK (source_record_id >= 1),
87
- source TEXT,
88
- op TEXT NOT NULL CHECK (op IN ('EDIT', 'SEND')),
89
- scheme TEXT,
90
- hostname TEXT,
91
- pathname TEXT,
92
- rx TEXT,
93
- attrs TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(attrs)),
94
- tags TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(tags) AND json_type(tags) = 'array'),
95
- status_rx INTEGER NOT NULL CHECK (status_rx BETWEEN 100 AND 599),
96
- terminated_by TEXT CHECK (terminated_by IS NULL OR terminated_by = 'cancel'),
97
- created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
136
+ id INTEGER NOT NULL PRIMARY KEY,
137
+ workspace_id INTEGER NOT NULL,
138
+ producer_worker_id INTEGER NOT NULL,
139
+ target_parent_worker_id INTEGER,
140
+ workspace_broadcast INTEGER NOT NULL DEFAULT 0 CHECK (workspace_broadcast IN (0, 1)),
141
+ kind TEXT NOT NULL CHECK (kind IN ('activity', 'loop_termination')),
142
+ source_record_id INTEGER NOT NULL CHECK (source_record_id >= 1),
143
+ at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
144
+ source TEXT,
145
+ op TEXT NOT NULL,
146
+ delimiter TEXT NOT NULL DEFAULT '',
147
+ signal TEXT CHECK (signal IS NULL OR json_valid(signal)),
148
+ scheme TEXT,
149
+ username TEXT,
150
+ password TEXT,
151
+ hostname TEXT,
152
+ port INTEGER CHECK (port IS NULL OR (port BETWEEN 0 AND 65535)),
153
+ pathname TEXT,
154
+ query TEXT,
155
+ fragment TEXT,
156
+ line_marker TEXT CHECK (line_marker IS NULL OR json_valid(line_marker)),
157
+ tx TEXT NOT NULL,
158
+ mimetype_tx TEXT NOT NULL CHECK (length(mimetype_tx) > 0),
159
+ rx TEXT NOT NULL,
160
+ mimetype_rx TEXT NOT NULL CHECK (length(mimetype_rx) > 0),
161
+ status_rx INTEGER NOT NULL CHECK (status_rx BETWEEN 100 AND 599),
162
+ state TEXT NOT NULL CHECK (state IN ('resolved', 'failed', 'cancelled')),
163
+ outcome TEXT,
164
+ attrs TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(attrs)),
165
+ tags TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(tags) AND json_type(tags) = 'array'),
166
+ terminated_by TEXT CHECK (terminated_by IS NULL OR terminated_by = 'cancel'),
167
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
168
+ CHECK (target_parent_worker_id IS NOT NULL OR workspace_broadcast = 1),
98
169
  CHECK (
99
- (kind = 'edit' AND op = 'EDIT' AND rx IS NOT NULL AND terminated_by IS NULL)
170
+ (kind = 'activity' AND terminated_by IS NULL)
100
171
  OR
101
172
  (kind = 'loop_termination'
102
173
  AND op = 'SEND'
103
174
  AND scheme = 'worker'
104
- AND rx IS NOT NULL
105
175
  AND json_valid(rx)
106
176
  AND json_type(rx) = 'object'
107
177
  AND json_type(rx, '$.status') = 'integer'
108
178
  AND json_extract(rx, '$.status') = status_rx)
109
179
  ),
110
- FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE,
111
- FOREIGN KEY (producer_worker_id) REFERENCES workers(id) ON DELETE CASCADE
180
+ FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE,
181
+ FOREIGN KEY (producer_worker_id) REFERENCES workers(id) ON DELETE CASCADE,
182
+ FOREIGN KEY (target_parent_worker_id) REFERENCES workers(id) ON DELETE CASCADE
112
183
  ) STRICT;
113
184
 
114
185
  CREATE INDEX IF NOT EXISTS ambient_events_workspace_id_id
115
186
  ON ambient_events (workspace_id, id);
116
187
  CREATE INDEX IF NOT EXISTS ambient_events_producer_kind_id
117
188
  ON ambient_events (producer_worker_id, kind, id);
189
+ CREATE INDEX IF NOT EXISTS ambient_events_parent_id
190
+ ON ambient_events (target_parent_worker_id, id)
191
+ WHERE target_parent_worker_id IS NOT NULL;
192
+ CREATE UNIQUE INDEX IF NOT EXISTS ambient_events_source_identity
193
+ ON ambient_events (producer_worker_id, kind, source_record_id);
194
+
195
+ CREATE TRIGGER IF NOT EXISTS ambient_events_structural_audience
196
+ BEFORE INSERT ON ambient_events
197
+ WHEN NOT EXISTS (
198
+ SELECT 1
199
+ FROM workers producer
200
+ WHERE producer.id = NEW.producer_worker_id
201
+ AND producer.workspace_id = NEW.workspace_id
202
+ AND (
203
+ NEW.target_parent_worker_id IS NULL
204
+ OR NEW.target_parent_worker_id = producer.parent_worker_id
205
+ )
206
+ )
207
+ BEGIN
208
+ SELECT RAISE(ABORT, 'ambient event audience must be the producer direct parent in the same workspace');
209
+ END;
210
+
211
+ -- A worker begins after the history that predates its existence. This trigger
212
+ -- runs in the worker INSERT statement, so an occurrence is either in the
213
+ -- baseline or after it. Fork INSERTs supply their own cursor/boundary and skip
214
+ -- this ordinary-worker baseline. {§env-delta-log-pull}
215
+ CREATE TRIGGER IF NOT EXISTS workers_capture_ambient_baseline
216
+ AFTER INSERT ON workers
217
+ WHEN NEW.ambient_event_cursor IS NULL
218
+ BEGIN
219
+ UPDATE workers
220
+ SET ambient_event_cursor = COALESCE((
221
+ SELECT MAX(ae.id) FROM ambient_events ae WHERE ae.workspace_id = NEW.workspace_id
222
+ ), 0)
223
+ WHERE id = NEW.id;
224
+ END;
118
225
 
119
226
  -- loops
120
227
  -- flags: per-loop runtime flags (auto, noProposals, noWeb, noInteraction,
@@ -128,9 +235,15 @@ CREATE TABLE IF NOT EXISTS loops (
128
235
  sequence INTEGER NOT NULL CHECK (sequence >= 1),
129
236
  status INTEGER NOT NULL DEFAULT 102 CHECK (status IN (100, 102, 200, 202, 413, 429, 499, 500, 504, 508)),
130
237
  prompt TEXT NOT NULL,
238
+ -- {§prompt-causal-source}: canonical actor address for the initial prompt;
239
+ -- NULL means the owning worker itself.
240
+ prompt_source TEXT CHECK (prompt_source IS NULL OR length(prompt_source) > 0),
131
241
  flags TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(flags)),
132
- provider_spec TEXT NOT NULL DEFAULT 'null' CHECK (json_valid(provider_spec)),
133
- child_provider_spec TEXT NOT NULL DEFAULT 'null' CHECK (json_valid(child_provider_spec)),
242
+ -- {§worker-model-selection}: immutable loop snapshots of the resolved model route and the
243
+ -- effective spawn route (was provider_spec/child_provider_spec JSON).
244
+ model_route_id INTEGER REFERENCES model_routes(id),
245
+ spawn_model_route_id INTEGER REFERENCES model_routes(id),
246
+ reasoning_policy TEXT CHECK (reasoning_policy IS NULL OR reasoning_policy IN ('off', 'adaptive', 'low', 'medium', 'high')),
134
247
  max_turns INTEGER NOT NULL DEFAULT 50 CHECK (max_turns >= -1),
135
248
  -- {§methods-loop-run-open-paths}: the initial prompt frame's selected paths,
136
249
  -- held here until turn 1 materializes that frame (string[] JSON).
@@ -145,6 +258,9 @@ CREATE TABLE IF NOT EXISTS loops (
145
258
  -- {§loop-terminal-authorship}: 'cancel' names an external loop.cancel;
146
259
  -- NULL covers model terminals and engine verdicts whose result carries the story.
147
260
  terminated_by TEXT CHECK (terminated_by IS NULL OR terminated_by = 'cancel'),
261
+ CONSTRAINT loops_generation_policy_contract CHECK (
262
+ (model_route_id IS NULL) = (reasoning_policy IS NULL)
263
+ ),
148
264
  CONSTRAINT loops_terminal_result_contract CHECK (
149
265
  CASE
150
266
  WHEN status IN (100, 102, 202) THEN terminal_result IS NULL
@@ -191,7 +307,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS loops_worker_id_sequence ON loops (worker_id,
191
307
  CREATE UNIQUE INDEX IF NOT EXISTS loops_orphan_source_loop_id ON loops (orphan_source_loop_id);
192
308
  -- {§worker-scheme}: a loop crossing into a terminal status stamps terminated_at, so sibling
193
309
  -- workers pull the termination as a folded ambient delta — caught uniformly across every
194
- -- death-path (SEND, grinder, max-turns, strike, KILL). The stamp updates terminated_at,
310
+ -- death-path (SEND, overflow recovery, max-turns, strike, KILL). The stamp updates terminated_at,
195
311
  -- never status, so it cannot re-fire this trigger. Terminals: 200 done · 413 budget ·
196
312
  -- 429 turn-ceiling · 499 cancel · 500 fail · 504 wall-clock timeout · 508 runaway. (202 = parked/sleeping, NOT terminal.)
197
313
  CREATE TRIGGER IF NOT EXISTS loops_stamp_terminated_at
@@ -201,22 +317,33 @@ BEGIN
201
317
  UPDATE loops SET terminated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = NEW.id;
202
318
  END;
203
319
 
204
- -- A terminal transition is an ambient occurrence in the same total order as a
205
- -- shared EDIT. Directly inserted fork history never crosses this transition and
320
+ -- A child terminal transition is an occurrence addressed only to its direct
321
+ -- parent. Directly inserted fork history never crosses this transition and
206
322
  -- therefore cannot fabricate a new conclusion event.
207
323
  CREATE TRIGGER IF NOT EXISTS loops_append_ambient_event
208
324
  AFTER UPDATE OF status ON loops
209
325
  WHEN NEW.status IN (200, 413, 429, 499, 500, 504, 508) AND OLD.status NOT IN (200, 413, 429, 499, 500, 504, 508)
210
326
  BEGIN
211
327
  INSERT INTO ambient_events (
212
- workspace_id, producer_worker_id, kind, source_record_id, source,
213
- op, scheme, pathname, rx, status_rx, terminated_by
328
+ workspace_id, producer_worker_id, target_parent_worker_id,
329
+ workspace_broadcast, kind, source_record_id, source,
330
+ op, delimiter, scheme, pathname,
331
+ tx, mimetype_tx, rx, mimetype_rx, status_rx, state, terminated_by
214
332
  )
215
- SELECT w.workspace_id, NEW.worker_id, 'loop_termination', NEW.id, NULL,
216
- 'SEND', 'worker', '/' || w.name, NEW.terminal_result,
217
- json_extract(NEW.terminal_result, '$.status'), NEW.terminated_by
333
+ SELECT w.workspace_id, NEW.worker_id, w.parent_worker_id,
334
+ 0, 'loop_termination', NEW.id, NULL,
335
+ 'SEND', '', 'worker', '/' || w.name,
336
+ '', 'text/plain', NEW.terminal_result, 'application/json',
337
+ json_extract(NEW.terminal_result, '$.status'), 'resolved', NEW.terminated_by
218
338
  FROM workers w
219
- WHERE w.id = NEW.worker_id;
339
+ WHERE w.id = NEW.worker_id
340
+ AND w.parent_worker_id IS NOT NULL
341
+ AND EXISTS (
342
+ SELECT 1
343
+ FROM turns t
344
+ WHERE t.loop_id = NEW.id
345
+ AND NOT (t.producer = '_plurnk' AND t.kind = 'operation')
346
+ );
220
347
  END;
221
348
 
222
349
  -- turns
@@ -229,7 +356,15 @@ CREATE TABLE IF NOT EXISTS turns (
229
356
  loop_id INTEGER NOT NULL,
230
357
  sequence INTEGER NOT NULL CHECK (sequence >= 1),
231
358
  timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
359
+ -- {§turn-record}: producer and purpose define the turn. Packet/provider
360
+ -- evidence below is an optional inference specialization.
361
+ producer TEXT NOT NULL CHECK (producer IN ('model', 'client', '_plurnk', 'plugin')),
362
+ kind TEXT NOT NULL CHECK (kind IN ('inference', 'initialization', 'overflow', 'operation', 'maintenance')),
232
363
  status INTEGER NOT NULL CHECK (status BETWEEN 100 AND 599),
364
+ -- NULL while the producer still owns this turn. Status 102 is both the
365
+ -- provisional running value and the exact completed continue disposition;
366
+ -- completed_at distinguishes those states without inventing another status.
367
+ completed_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
233
368
  -- Provider-derived curation calibration for this turn; NULL when input
234
369
  -- capacity is unknown. {§tokenomics-client-gauge}
235
370
  usage_curation_budget INTEGER CHECK (usage_curation_budget IS NULL OR usage_curation_budget >= 1),
@@ -264,16 +399,59 @@ CREATE TABLE IF NOT EXISTS turns (
264
399
  END
265
400
  ),
266
401
  finish_reason TEXT,
267
- model TEXT NOT NULL DEFAULT 'unknown' CHECK (length(model) >= 1),
268
- -- Opaque provider→client metadata plus engine rail keys; JSON-valid but
269
- -- otherwise unenforced. {§meta-passthrough}, {§rail-truth-engine-verdict}
270
- meta TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(meta)),
402
+ model TEXT CHECK (model IS NULL OR length(model) >= 1),
403
+ -- Opaque provider→client metadata plus engine rail keys; absent outside
404
+ -- recorded inference evidence. {§meta-passthrough}, {§rail-truth-engine-verdict}
405
+ meta TEXT CHECK (meta IS NULL OR json_valid(meta)),
406
+ CHECK (completed_at IS NOT NULL OR status = 102),
407
+ CHECK ((producer = 'model') = (kind = 'inference')),
408
+ CHECK (kind NOT IN ('initialization', 'overflow') OR producer = '_plurnk'),
409
+ CHECK (
410
+ kind = 'inference'
411
+ OR (
412
+ usage_curation_budget IS NULL
413
+ AND packet IS NULL
414
+ AND finish_reason IS NULL
415
+ AND model IS NULL
416
+ AND meta IS NULL
417
+ )
418
+ ),
271
419
  FOREIGN KEY (loop_id) REFERENCES loops(id) ON DELETE CASCADE
272
420
  ) STRICT;
273
421
 
274
422
  CREATE UNIQUE INDEX IF NOT EXISTS turns_loop_id_sequence ON turns (loop_id, sequence);
275
423
  CREATE INDEX IF NOT EXISTS turns_timestamp ON turns (timestamp);
276
424
 
425
+ -- Producer and purpose are immutable except for the single pre-inference
426
+ -- diversion into overflow recovery. The transition is allowed only before any
427
+ -- model or non-kernel evidence exists.
428
+ CREATE TRIGGER IF NOT EXISTS turns_identity_forward_only
429
+ BEFORE UPDATE OF producer, kind ON turns
430
+ WHEN NOT (
431
+ OLD.producer = NEW.producer
432
+ AND OLD.kind = NEW.kind
433
+ )
434
+ AND NOT (
435
+ OLD.producer = 'model'
436
+ AND OLD.kind = 'inference'
437
+ AND NEW.producer = '_plurnk'
438
+ AND NEW.kind = 'overflow'
439
+ AND OLD.completed_at IS NULL
440
+ AND OLD.packet IS NULL
441
+ AND OLD.usage_curation_budget IS NULL
442
+ AND OLD.finish_reason IS NULL
443
+ AND OLD.model IS NULL
444
+ AND OLD.meta IS NULL
445
+ AND NOT EXISTS (SELECT 1 FROM model_calls WHERE turn_id = OLD.id)
446
+ AND NOT EXISTS (
447
+ SELECT 1 FROM log_entries
448
+ WHERE turn_id = OLD.id AND origin != '_plurnk'
449
+ )
450
+ )
451
+ BEGIN
452
+ SELECT RAISE(ABORT, 'turn producer and kind are immutable outside pre-inference overflow diversion');
453
+ END;
454
+
277
455
  -- One logical provider.generate call. Emission attempts and BARE inferences
278
456
  -- share response/failure evidence and cardinal physical request accounting;
279
457
  -- operation-specific semantics live in their specializing relations.
@@ -321,6 +499,17 @@ CREATE TABLE IF NOT EXISTS model_calls (
321
499
 
322
500
  CREATE INDEX IF NOT EXISTS model_calls_turn_id ON model_calls (turn_id, sequence);
323
501
 
502
+ CREATE TRIGGER IF NOT EXISTS model_calls_inference_turn_only
503
+ BEFORE INSERT ON model_calls
504
+ WHEN COALESCE((
505
+ SELECT producer = 'model' AND kind = 'inference'
506
+ FROM turns
507
+ WHERE id = NEW.turn_id
508
+ ), 0) != 1
509
+ BEGIN
510
+ SELECT RAISE(ABORT, 'model call requires a model inference turn');
511
+ END;
512
+
324
513
  CREATE TRIGGER IF NOT EXISTS model_calls_request_identity_immutable
325
514
  BEFORE UPDATE OF turn_id, sequence, kind, attributions, timestamp ON model_calls
326
515
  BEGIN
@@ -532,9 +721,9 @@ BEGIN
532
721
  END;
533
722
 
534
723
  -- derivations
535
- -- Content-addressed deep projections. Entries point at a COMPLETE artifact by
536
- -- deep_hash; graph, FTS, and vectors are stored once regardless of how many
537
- -- workspace/worktree paths carry identical content under the same reader/config.
724
+ -- Content-addressed deep projections. Entry channels and log projections point
725
+ -- at a COMPLETE artifact by deep_hash; graph, FTS, and vectors are stored once
726
+ -- regardless of how many addresses carry identical content under the same reader/config.
538
727
  -- A building row is unattached and safely replaceable after interruption.
539
728
  CREATE TABLE IF NOT EXISTS derivations (
540
729
  id INTEGER NOT NULL PRIMARY KEY,
@@ -543,19 +732,30 @@ CREATE TABLE IF NOT EXISTS derivations (
543
732
  disposition TEXT CHECK (disposition IN ('vector', 'lexical', 'excluded', 'nonsemantic', 'failed')),
544
733
  reason TEXT,
545
734
  parse_issues INTEGER CHECK (parse_issues IS NULL OR parse_issues > 0),
735
+ summary TEXT CHECK (
736
+ summary IS NULL OR (
737
+ length(summary) > 0
738
+ AND summary = trim(summary)
739
+ AND instr(summary, char(10)) = 0
740
+ AND instr(summary, char(13)) = 0
741
+ )
742
+ ),
546
743
  CHECK ((state = 'building' AND disposition IS NULL) OR (state = 'complete' AND disposition IS NOT NULL))
547
744
  ) STRICT;
548
745
 
549
746
  -- entries
550
- -- The canonical addressable store. (workspace, owner, scheme, pathname) is the identity
747
+ -- The canonical addressable store. (owner, scheme, authority, pathname) is the identity
551
748
  -- tuple, and NO component may be NULL: NULLs are distinct under SQL UNIQUE, so a nullable
552
749
  -- component voids the identity index. Bare/file paths persist under the reserved `file`
553
750
  -- scheme; they still render as bare paths. {§entry-identity-no-null}
554
751
  CREATE TABLE IF NOT EXISTS entries (
555
752
  id INTEGER NOT NULL PRIMARY KEY,
556
753
  version INTEGER NOT NULL DEFAULT 0 CHECK (version >= 0),
557
- workspace_id INTEGER,
558
754
  scheme TEXT NOT NULL CHECK (length(scheme) > 0),
755
+ -- Canonical RFC authority for resource-addressed schemes. Namespace schemes
756
+ -- fold authored authority into pathname; owner-addressed schemes consume it
757
+ -- into owner_id. Empty is therefore the canonical no-resource-authority value.
758
+ authority TEXT NOT NULL DEFAULT '',
559
759
  pathname TEXT NOT NULL,
560
760
  -- {§entry-owner} — every entry is owned by a worker: the spawning worker for capability
561
761
  -- streams, the workspace's reserved 'commons' worker for shared content. A real row, never
@@ -569,14 +769,9 @@ CREATE TABLE IF NOT EXISTS entries (
569
769
  -- SPEC {§membership} — how a file member entered the curated surface. 'git' rows are
570
770
  -- reconciled against the repo's members each turn — tracked ls-files PLUS untracked-
571
771
  -- but-not-ignored files ({§membership-auto-add}) — registered + un-registered so entries
572
- -- == members; 'client'/'constraint' (model-created, add-glob) are not git's to reclaim.
772
+ -- == members; 'constraint' rows are reconciled from ordinary pick policy.
573
773
  -- NULL = not a file member (other schemes don't carry origin).
574
- membership_origin TEXT CHECK (membership_origin IS NULL OR membership_origin IN ('git', 'client', 'constraint')),
575
- -- {§membership-change-gated-sync}: hash of the body content at the last
576
- -- deep-channel derivation. The manifest-add pass re-derives symbols/refs (and
577
- -- embeddings, later) ONLY when this differs from the current body hash — an
578
- -- unchanged entry is skipped, never re-metadatafied every turn.
579
- deep_hash TEXT,
774
+ membership_origin TEXT CHECK (membership_origin IS NULL OR membership_origin IN ('git', 'constraint')),
580
775
  -- SPEC {§membership-change-gated-sync} — the per-member sync stat-detect:
581
776
  -- "<mtimeMs>:<size>" of the disk file at its last materialization, or `absent`
582
777
  -- after an observed deletion. The pre-turn
@@ -584,20 +779,19 @@ CREATE TABLE IF NOT EXISTS entries (
584
779
  -- signature changed; an unchanged member is a no-op. NULL = never synced.
585
780
  synced_sig TEXT,
586
781
  -- User Note 5 — manifest cache-friendliness. Last-modified stamp, bumped on every
587
- -- channel write by entries_touch_on_channel_write; engine_list_workspace_entries orders
588
- -- the catalog by it ASC so dormant entries hold the stable prompt-cache prefix.
782
+ -- addressable representation change; engine_list_workspace_entries orders the catalog
783
+ -- by it ASC so dormant entries hold the stable prompt-cache prefix. Private derivation
784
+ -- attachment does not make an entry recently touched.
589
785
  updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
590
- CHECK (workspace_id IS NOT NULL),
591
- FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE,
592
- FOREIGN KEY (owner_id) REFERENCES workers(id) ON DELETE CASCADE,
593
- FOREIGN KEY (deep_hash) REFERENCES derivations(deep_hash)
786
+ FOREIGN KEY (owner_id) REFERENCES workers(id) ON DELETE CASCADE
594
787
  ) STRICT;
595
788
 
596
789
  -- {§entry-owner} / {§stream-owner-scoped} — owner is the sole visibility and identity axis.
597
790
  -- Concurrent workers' capability streams share the loop-relative coordinate (every worker's first
598
791
  -- loop is seq 1), so identity keys on the owner and identical coordinates are distinct rows
599
792
  -- ({§stream-owner-scoped}).
600
- CREATE UNIQUE INDEX IF NOT EXISTS entries_identity ON entries (workspace_id, owner_id, scheme, pathname);
793
+ CREATE UNIQUE INDEX IF NOT EXISTS entries_identity ON entries (owner_id, scheme, authority, pathname);
794
+ CREATE UNIQUE INDEX IF NOT EXISTS entries_id_owner ON entries (id, owner_id);
601
795
 
602
796
  -- The ONE engine-imposed constraint (SPEC {§stream-constraints}, {§stream-constraints-engine-one-cap}): 100 MiB char-length cap
603
797
  -- per channel content body. All other limits are extrinsic.
@@ -610,6 +804,9 @@ CREATE TABLE IF NOT EXISTS entry_channels (
610
804
  -- Content identity: sha256 of content, stamped at static writes; streamed
611
805
  -- appends leave it NULL. Curation weight remains model-independent.
612
806
  content_hash TEXT,
807
+ -- Search derivation for this exact addressable channel representation.
808
+ -- Content or mimetype changes invalidate it at the owning channel row.
809
+ deep_hash TEXT,
613
810
  state TEXT NOT NULL DEFAULT 'static' CHECK (state IN ('static', 'active', 'closed', 'errored')),
614
811
  -- Exact terminal producer evidence for this representation channel. NULL is
615
812
  -- the ordinary implicit {status:200}; selection/projection metadata belongs
@@ -642,12 +839,26 @@ CREATE TABLE IF NOT EXISTS entry_channels (
642
839
  END
643
840
  ),
644
841
  PRIMARY KEY (entry_id, name),
645
- FOREIGN KEY (entry_id) REFERENCES entries(id) ON DELETE CASCADE
842
+ FOREIGN KEY (entry_id) REFERENCES entries(id) ON DELETE CASCADE,
843
+ FOREIGN KEY (deep_hash) REFERENCES derivations(deep_hash)
646
844
  ) STRICT, WITHOUT ROWID;
647
845
 
648
- -- User Note 5 — bump the entry's updated_at on any channel write so the catalog
649
- -- (ordered by updated_at ASC) keeps recently-touched entries at the tail and holds
650
- -- the prompt-cache prefix stable across turns.
846
+ -- A changed channel representation cannot retain search evidence derived from
847
+ -- its predecessor. This trigger is the one invalidation owner for every write
848
+ -- path, including model EDIT, plugin channel capabilities, and streams.
849
+ CREATE TRIGGER IF NOT EXISTS entry_channels_invalidate_derivation
850
+ AFTER UPDATE OF content, mimetype ON entry_channels
851
+ WHEN OLD.content IS NOT NEW.content OR OLD.mimetype IS NOT NEW.mimetype
852
+ BEGIN
853
+ UPDATE entry_channels
854
+ SET deep_hash = NULL
855
+ WHERE entry_id = NEW.entry_id AND name = NEW.name AND deep_hash IS NOT NULL;
856
+ END;
857
+
858
+ -- User Note 5 — bump the entry's updated_at on addressable representation or
859
+ -- lifecycle writes so the catalog (ordered by updated_at ASC) keeps recently-
860
+ -- touched entries at the tail and holds the prompt-cache prefix stable across
861
+ -- turns. Content hashes and search attachments are private metadata, not touches.
651
862
  CREATE TRIGGER IF NOT EXISTS entries_touch_on_channel_write
652
863
  AFTER INSERT ON entry_channels
653
864
  BEGIN
@@ -655,7 +866,7 @@ BEGIN
655
866
  END;
656
867
 
657
868
  CREATE TRIGGER IF NOT EXISTS entries_touch_on_channel_update
658
- AFTER UPDATE ON entry_channels
869
+ AFTER UPDATE OF content, mimetype, weight, state, producer_result ON entry_channels
659
870
  BEGIN
660
871
  UPDATE entries SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = NEW.entry_id;
661
872
  END;
@@ -731,7 +942,8 @@ CREATE TABLE IF NOT EXISTS derivation_embeddings (
731
942
  -- status⊥state: status is the HTTP outcome, state is where in the
732
943
  -- lifecycle the entry sits. Most rows write 'resolved' directly;
733
944
  -- proposing schemes transition 'proposed' → resolved/failed/cancelled.
734
- -- expanded: per-row visibility for OPEN/FOLD via the log:/// scheme.
945
+ -- folded: canonical hidden log-body line intervals for OPEN/FOLD. [] is
946
+ -- wholly open and [[1,-1]] is wholly folded.
735
947
  CREATE TABLE IF NOT EXISTS log_entries (
736
948
  id INTEGER NOT NULL PRIMARY KEY,
737
949
  version INTEGER NOT NULL DEFAULT 0 CHECK (version >= 0),
@@ -741,30 +953,38 @@ CREATE TABLE IF NOT EXISTS log_entries (
741
953
  turn_id INTEGER NOT NULL,
742
954
  sequence INTEGER NOT NULL CHECK (sequence >= 1),
743
955
  at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
744
- origin TEXT NOT NULL CHECK (origin IN ('model', 'client', 'plurnk', 'plugin')),
956
+ origin TEXT NOT NULL CHECK (origin IN ('model', 'client', '_plurnk', 'plugin')),
745
957
  -- {§env-delta-attribution}: a worker:// name or stable subsystem token ('file');
746
958
  -- NULL = the owning worker itself, rendered without causal attribution.
747
959
  source TEXT,
748
960
  -- Engine-owned occurrence identity. Source rows are stamped NULL→id by the
749
961
  -- journal trigger; observer and fork copies carry it at insertion.
750
962
  ambient_event_id INTEGER REFERENCES ambient_events(id),
963
+ -- Fork history is copied evidence, never new activity. Rows with no source
964
+ -- occurrence still need this structural guard against republication.
965
+ inherited_history INTEGER NOT NULL DEFAULT 0 CHECK (inherited_history IN (0, 1)),
751
966
  -- Search derivation attached to this durable log result, when available.
752
967
  deep_hash TEXT REFERENCES derivations(deep_hash),
753
- -- Exact logical provider call represented by a BARE result or model-emission
754
- -- mirror. Other operation and ambient rows carry no model-call identity.
968
+ -- Exact logical provider call represented by a BARE result, admitted
969
+ -- model turnOps, or rejected emissionAttempt. Other operation and ambient
970
+ -- rows carry no model-call identity.
755
971
  model_call_id INTEGER REFERENCES model_calls(id),
972
+ -- Engine-owned stream publication that produced this observation. The
973
+ -- publication cursor survives log curation; this relation is evidence,
974
+ -- never the acknowledgement itself. {§exec-stream}
975
+ subscription_publication_id INTEGER REFERENCES subscription_publications(id) ON DELETE SET NULL,
756
976
 
757
977
  -- 'error' is an ACTIONLESS row ({§operation-results} — errors are log items): a parse failure that
758
978
  -- produced no op still records a log entry (op='error', status_rx≥400, no target) so the model
759
979
  -- can fold/kill/recall its own mistakes like any other log row — one budget surface, the log.
760
- -- Actionless artifacts carry NULL here: kernel-authored worker initialization
761
- -- ({§worker-initialization-entry}) or a model emission ({§model-entry}).
980
+ -- Exact producer source and rejected-attempt artifacts carry NULL here
981
+ -- ({§turn-ops-entry}, {§rejected-emission-entry}).
762
982
  -- No op enum here: the grammar op set is grammar's contract (PlurnkOp), and this column is written
763
983
  -- only by the PlurnkOp-typed engine (grammar ops), service row selectors, or NULL for no op.
764
984
  -- A SQL enum would be a hand-copy of grammar's op list that silently goes stale on every new verb
765
985
  -- (it did — FORK/WORK). Validity lives at the parse + type layer, not duplicated in DDL.
766
986
  op TEXT,
767
- suffix TEXT NOT NULL DEFAULT '',
987
+ delimiter TEXT NOT NULL DEFAULT '',
768
988
  signal TEXT CHECK (signal IS NULL OR json_valid(signal)),
769
989
 
770
990
  scheme TEXT CHECK (scheme IS NULL OR length(scheme) > 0),
@@ -795,11 +1015,16 @@ CREATE TABLE IF NOT EXISTS log_entries (
795
1015
  outcome TEXT,
796
1016
  attrs TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(attrs)),
797
1017
 
798
- expanded INTEGER NOT NULL DEFAULT 1 CHECK (expanded IN (0, 1)),
1018
+ folded TEXT NOT NULL DEFAULT '[]'
1019
+ CHECK (json_valid(folded) AND json_type(folded) = 'array'),
799
1020
 
800
- CHECK ((op IS NULL) = COALESCE(json_extract(attrs, '$.kind') IN ('initialization', 'model_emission'), 0)),
801
- CHECK (json_extract(attrs, '$.kind') != 'initialization' OR origin = 'plurnk'),
802
- CHECK (json_extract(attrs, '$.kind') != 'model_emission' OR origin = 'model'),
1021
+ CHECK (
1022
+ (op IS NULL) = COALESCE(
1023
+ json_extract(attrs, '$.kind') IN ('turnOps', 'emissionAttempt'),
1024
+ 0
1025
+ )
1026
+ ),
1027
+ CHECK (json_extract(attrs, '$.kind') != 'emissionAttempt' OR origin = 'model'),
803
1028
 
804
1029
  FOREIGN KEY (worker_id) REFERENCES workers(id) ON DELETE CASCADE,
805
1030
  FOREIGN KEY (loop_id) REFERENCES loops(id) ON DELETE CASCADE,
@@ -813,10 +1038,115 @@ CREATE INDEX IF NOT EXISTS log_entries_at ON log_entries (a
813
1038
  CREATE UNIQUE INDEX IF NOT EXISTS log_entries_model_call_id
814
1039
  ON log_entries (model_call_id)
815
1040
  WHERE model_call_id IS NOT NULL;
1041
+ CREATE INDEX IF NOT EXISTS log_entries_subscription_publication_id
1042
+ ON log_entries (subscription_publication_id)
1043
+ WHERE subscription_publication_id IS NOT NULL;
816
1044
  CREATE UNIQUE INDEX IF NOT EXISTS log_entries_worker_ambient_event
817
1045
  ON log_entries (worker_id, ambient_event_id)
818
1046
  WHERE ambient_event_id IS NOT NULL;
819
1047
 
1048
+ -- A log row belongs to one exact worker/loop/turn chain. Its writer is either
1049
+ -- that turn's producer or `_plurnk` observing the turn. Primitive absence,
1050
+ -- enum, and foreign-key failures remain owned by their column constraints.
1051
+ CREATE TRIGGER IF NOT EXISTS log_entries_turn_ownership
1052
+ BEFORE INSERT ON log_entries
1053
+ WHEN NEW.worker_id IS NOT NULL
1054
+ AND NEW.loop_id IS NOT NULL
1055
+ AND NEW.turn_id IS NOT NULL
1056
+ AND NEW.origin IN ('model', 'client', '_plurnk', 'plugin')
1057
+ AND EXISTS (SELECT 1 FROM workers WHERE id = NEW.worker_id)
1058
+ AND EXISTS (SELECT 1 FROM loops WHERE id = NEW.loop_id)
1059
+ AND EXISTS (SELECT 1 FROM turns WHERE id = NEW.turn_id)
1060
+ AND NOT EXISTS (
1061
+ SELECT 1
1062
+ FROM turns
1063
+ JOIN loops ON loops.id = turns.loop_id
1064
+ WHERE turns.id = NEW.turn_id
1065
+ AND loops.id = NEW.loop_id
1066
+ AND loops.worker_id = NEW.worker_id
1067
+ AND (NEW.origin = turns.producer OR NEW.origin = '_plurnk')
1068
+ )
1069
+ BEGIN
1070
+ SELECT RAISE(ABORT, 'log entry must match its turn ownership and producer');
1071
+ END;
1072
+
1073
+ -- {§turn-ops-entry} — exact admitted source is authored by the turn producer.
1074
+ -- `_plurnk` may observe another producer's turn through ordinary operation
1075
+ -- rows, but it cannot impersonate that producer's source program.
1076
+ CREATE TRIGGER IF NOT EXISTS log_entries_turn_ops_producer
1077
+ BEFORE INSERT ON log_entries
1078
+ WHEN NEW.op IS NULL
1079
+ AND json_extract(NEW.attrs, '$.kind') = 'turnOps'
1080
+ AND EXISTS (SELECT 1 FROM turns WHERE id = NEW.turn_id)
1081
+ AND NOT EXISTS (
1082
+ SELECT 1
1083
+ FROM turns
1084
+ WHERE turns.id = NEW.turn_id
1085
+ AND turns.producer = NEW.origin
1086
+ )
1087
+ BEGIN
1088
+ SELECT RAISE(ABORT, 'actionless log entry does not match its turn producer');
1089
+ END;
1090
+
1091
+ -- A folded interval is an inclusive [start,end] pair. Ranges are positive,
1092
+ -- sorted, disjoint, and non-adjacent; -1 is the final open-ended endpoint.
1093
+ -- Canonical intervals make visibility equality and curation effects exact.
1094
+ CREATE TRIGGER IF NOT EXISTS log_entries_folded_valid_insert
1095
+ BEFORE INSERT ON log_entries
1096
+ WHEN EXISTS (
1097
+ SELECT 1
1098
+ FROM json_each(NEW.folded) range
1099
+ WHERE range.type != 'array'
1100
+ OR json_array_length(range.value) != 2
1101
+ OR COALESCE(json_type(range.value, '$[0]'), '') != 'integer'
1102
+ OR COALESCE(json_type(range.value, '$[1]'), '') != 'integer'
1103
+ OR json_extract(range.value, '$[0]') < 1
1104
+ OR (
1105
+ json_extract(range.value, '$[1]') != -1
1106
+ AND json_extract(range.value, '$[1]') < json_extract(range.value, '$[0]')
1107
+ )
1108
+ OR EXISTS (
1109
+ SELECT 1
1110
+ FROM json_each(NEW.folded) previous
1111
+ WHERE previous.key = range.key - 1
1112
+ AND (
1113
+ json_extract(previous.value, '$[1]') = -1
1114
+ OR json_extract(range.value, '$[0]') <= json_extract(previous.value, '$[1]') + 1
1115
+ )
1116
+ )
1117
+ )
1118
+ BEGIN
1119
+ SELECT RAISE(ABORT, 'log entry folded ranges are invalid');
1120
+ END;
1121
+
1122
+ CREATE TRIGGER IF NOT EXISTS log_entries_folded_valid_update
1123
+ BEFORE UPDATE OF folded ON log_entries
1124
+ WHEN EXISTS (
1125
+ SELECT 1
1126
+ FROM json_each(NEW.folded) range
1127
+ WHERE range.type != 'array'
1128
+ OR json_array_length(range.value) != 2
1129
+ OR COALESCE(json_type(range.value, '$[0]'), '') != 'integer'
1130
+ OR COALESCE(json_type(range.value, '$[1]'), '') != 'integer'
1131
+ OR json_extract(range.value, '$[0]') < 1
1132
+ OR (
1133
+ json_extract(range.value, '$[1]') != -1
1134
+ AND json_extract(range.value, '$[1]') < json_extract(range.value, '$[0]')
1135
+ )
1136
+ OR EXISTS (
1137
+ SELECT 1
1138
+ FROM json_each(NEW.folded) previous
1139
+ WHERE previous.key = range.key - 1
1140
+ AND (
1141
+ json_extract(previous.value, '$[1]') = -1
1142
+ OR json_extract(range.value, '$[0]') <= json_extract(previous.value, '$[1]') + 1
1143
+ )
1144
+ )
1145
+ )
1146
+ BEGIN
1147
+ SELECT RAISE(ABORT, 'log entry folded ranges are invalid');
1148
+ END;
1149
+
820
1150
  CREATE TRIGGER IF NOT EXISTS log_entries_model_call_valid
821
1151
  BEFORE INSERT ON log_entries
822
1152
  WHEN NEW.model_call_id IS NOT NULL
@@ -831,7 +1161,7 @@ WHEN NEW.model_call_id IS NOT NULL
831
1161
  OR (
832
1162
  call.kind = 'emission'
833
1163
  AND NEW.op IS NULL
834
- AND json_extract(NEW.attrs, '$.kind') = 'model_emission'
1164
+ AND json_extract(NEW.attrs, '$.kind') IN ('turnOps', 'emissionAttempt')
835
1165
  )
836
1166
  )
837
1167
  )
@@ -930,19 +1260,93 @@ BEGIN
930
1260
  SELECT json_group_array(tag)
931
1261
  FROM (SELECT tag FROM log_tags WHERE log_entry_id = NEW.id ORDER BY tag)
932
1262
  ), '[]')
933
- WHERE kind = 'edit'
1263
+ WHERE kind = 'activity'
1264
+ AND producer_worker_id = NEW.worker_id
1265
+ AND source_record_id = NEW.id;
1266
+ END;
1267
+
1268
+ -- {§worker-initialization-entry} {§overflow-turn-script} — the turn identity
1269
+ -- classifies every row in a kernel-authored recovery/orientation turn. No
1270
+ -- caller can omit provenance from either its exact turnOps or its operation
1271
+ -- outcomes.
1272
+ CREATE TRIGGER IF NOT EXISTS log_entries_classify_plurnk_turn
1273
+ AFTER INSERT ON log_entries
1274
+ WHEN EXISTS (
1275
+ SELECT 1
1276
+ FROM turns
1277
+ WHERE turns.id = NEW.turn_id
1278
+ AND turns.producer = '_plurnk'
1279
+ AND turns.kind IN ('initialization', 'overflow')
1280
+ )
1281
+ BEGIN
1282
+ INSERT OR IGNORE INTO log_tags (log_entry_id, tag)
1283
+ VALUES (NEW.id, '_plurnk');
1284
+ INSERT OR IGNORE INTO log_tags (log_entry_id, tag)
1285
+ SELECT NEW.id, CASE turns.kind
1286
+ WHEN 'initialization' THEN 'init'
1287
+ ELSE 'overflow'
1288
+ END
1289
+ FROM turns
1290
+ WHERE turns.id = NEW.turn_id;
1291
+
1292
+ UPDATE ambient_events
1293
+ SET tags = COALESCE((
1294
+ SELECT json_group_array(tag)
1295
+ FROM (SELECT tag FROM log_tags WHERE log_entry_id = NEW.id ORDER BY tag)
1296
+ ), '[]')
1297
+ WHERE kind = 'activity'
934
1298
  AND producer_worker_id = NEW.worker_id
935
1299
  AND source_record_id = NEW.id;
936
1300
  END;
937
1301
 
1302
+ -- Overflow admission reclassifies a would-be inference turn only after its
1303
+ -- pre-model rows exist. Apply the same turn-owned classification to those
1304
+ -- rows; later rows are covered by log_entries_classify_plurnk_turn above.
1305
+ CREATE TRIGGER IF NOT EXISTS turns_classify_existing_plurnk_rows
1306
+ AFTER UPDATE OF producer, kind ON turns
1307
+ WHEN NEW.producer = '_plurnk'
1308
+ AND NEW.kind IN ('initialization', 'overflow')
1309
+ BEGIN
1310
+ INSERT OR IGNORE INTO log_tags (log_entry_id, tag)
1311
+ SELECT id, '_plurnk' FROM log_entries WHERE turn_id = NEW.id;
1312
+ INSERT OR IGNORE INTO log_tags (log_entry_id, tag)
1313
+ SELECT id, CASE NEW.kind
1314
+ WHEN 'initialization' THEN 'init'
1315
+ ELSE 'overflow'
1316
+ END
1317
+ FROM log_entries
1318
+ WHERE turn_id = NEW.id;
1319
+
1320
+ UPDATE ambient_events
1321
+ SET tags = COALESCE((
1322
+ SELECT json_group_array(tag)
1323
+ FROM (
1324
+ SELECT lt.tag
1325
+ FROM log_tags lt
1326
+ WHERE lt.log_entry_id = ambient_events.source_record_id
1327
+ ORDER BY lt.tag
1328
+ )
1329
+ ), '[]')
1330
+ WHERE kind = 'activity'
1331
+ AND producer_worker_id = (
1332
+ SELECT l.worker_id FROM loops l WHERE l.id = NEW.loop_id
1333
+ )
1334
+ AND source_record_id IN (
1335
+ SELECT id FROM log_entries WHERE turn_id = NEW.id
1336
+ );
1337
+ END;
1338
+
938
1339
  -- Successful OPEN/FOLD rows are durable curation events even though their
939
1340
  -- ordinary packet projection is suppressed ({§fold-open-meta-operations}).
940
- -- Preserve the exact selected set and each target's pre-event visibility so a
1341
+ -- Preserve the exact selected set and each target's before/after visibility so a
941
1342
  -- broad selector never collapses into the lossy fact `matched: N`.
942
1343
  CREATE TABLE IF NOT EXISTS log_curation_effects (
943
1344
  operation_log_entry_id INTEGER NOT NULL,
944
1345
  target_log_entry_id INTEGER NOT NULL,
945
- expanded_before INTEGER NOT NULL CHECK (expanded_before IN (0, 1)),
1346
+ folded_before TEXT NOT NULL
1347
+ CHECK (json_valid(folded_before) AND json_type(folded_before) = 'array'),
1348
+ folded_after TEXT NOT NULL
1349
+ CHECK (json_valid(folded_after) AND json_type(folded_after) = 'array'),
946
1350
  tags_added TEXT NOT NULL DEFAULT '[]'
947
1351
  CHECK (json_valid(tags_added) AND json_type(tags_added) = 'array'),
948
1352
  tags_removed TEXT NOT NULL DEFAULT '[]'
@@ -972,6 +1376,39 @@ BEGIN
972
1376
  AND operation.status_rx < 400
973
1377
  AND operation.worker_id = target.worker_id
974
1378
  )
1379
+ OR EXISTS (
1380
+ SELECT 1
1381
+ FROM (
1382
+ SELECT 'before' AS side, key, value, type FROM json_each(NEW.folded_before)
1383
+ UNION ALL
1384
+ SELECT 'after' AS side, key, value, type FROM json_each(NEW.folded_after)
1385
+ ) range
1386
+ WHERE range.type != 'array'
1387
+ OR json_array_length(range.value) != 2
1388
+ OR COALESCE(json_type(range.value, '$[0]'), '') != 'integer'
1389
+ OR COALESCE(json_type(range.value, '$[1]'), '') != 'integer'
1390
+ OR json_extract(range.value, '$[0]') < 1
1391
+ OR (
1392
+ json_extract(range.value, '$[1]') != -1
1393
+ AND json_extract(range.value, '$[1]') < json_extract(range.value, '$[0]')
1394
+ )
1395
+ OR EXISTS (
1396
+ SELECT 1
1397
+ FROM (
1398
+ SELECT key, value FROM json_each(
1399
+ CASE range.side
1400
+ WHEN 'before' THEN NEW.folded_before
1401
+ ELSE NEW.folded_after
1402
+ END
1403
+ )
1404
+ ) previous
1405
+ WHERE previous.key = range.key - 1
1406
+ AND (
1407
+ json_extract(previous.value, '$[1]') = -1
1408
+ OR json_extract(range.value, '$[0]') <= json_extract(previous.value, '$[1]') + 1
1409
+ )
1410
+ )
1411
+ )
975
1412
  OR EXISTS (
976
1413
  SELECT 1
977
1414
  FROM (
@@ -1027,7 +1464,7 @@ BEGIN
1027
1464
  END;
1028
1465
 
1029
1466
  -- One outer INSERT owns the whole landed curation event: exact selected rows,
1030
- -- their pre-event visibility and tag deltas, the requested visibility, and the
1467
+ -- their before/after visibility and tag deltas, and the
1031
1468
  -- resulting classifications. Trigger failure rolls the operation row and all
1032
1469
  -- effects back together. The private plan is erased before INSERT returns.
1033
1470
  CREATE TRIGGER IF NOT EXISTS log_entries_apply_curation
@@ -1037,26 +1474,70 @@ WHEN NEW.op IN ('OPEN', 'FOLD')
1037
1474
  AND json_type(NEW.attrs, '$.__plurnk_curation') = 'object'
1038
1475
  BEGIN
1039
1476
  SELECT CASE WHEN
1040
- COALESCE(json_type(NEW.attrs, '$.__plurnk_curation.ids'), '') != 'array'
1477
+ COALESCE(json_type(NEW.attrs, '$.__plurnk_curation.targets'), '') != 'array'
1041
1478
  OR COALESCE(json_type(NEW.attrs, '$.__plurnk_curation.add'), '') != 'array'
1042
1479
  OR COALESCE(json_type(NEW.attrs, '$.__plurnk_curation.remove'), '') != 'array'
1043
- OR COALESCE(json_type(NEW.attrs, '$.__plurnk_curation.expanded'), '') != 'integer'
1044
- OR json_extract(NEW.attrs, '$.__plurnk_curation.expanded') NOT IN (0, 1)
1045
- OR json_array_length(NEW.attrs, '$.__plurnk_curation.ids') = 0
1480
+ OR json_array_length(NEW.attrs, '$.__plurnk_curation.targets') = 0
1046
1481
  OR EXISTS (
1047
- SELECT 1 FROM json_each(NEW.attrs, '$.__plurnk_curation.ids')
1048
- WHERE type != 'integer' OR value <= 0
1482
+ SELECT 1
1483
+ FROM json_each(NEW.attrs, '$.__plurnk_curation.targets') selected
1484
+ WHERE selected.type != 'object'
1485
+ OR COALESCE(json_type(selected.value, '$.id'), '') != 'integer'
1486
+ OR json_extract(selected.value, '$.id') <= 0
1487
+ OR COALESCE(json_type(selected.value, '$.before'), '') != 'array'
1488
+ OR COALESCE(json_type(selected.value, '$.after'), '') != 'array'
1489
+ OR EXISTS (
1490
+ SELECT 1 FROM json_each(selected.value) field
1491
+ WHERE field.key NOT IN ('id', 'before', 'after')
1492
+ )
1493
+ OR EXISTS (
1494
+ SELECT 1
1495
+ FROM (
1496
+ SELECT 'before' AS side, range.key, range.value, range.type
1497
+ FROM json_each(json_extract(selected.value, '$.before')) range
1498
+ UNION ALL
1499
+ SELECT 'after' AS side, range.key, range.value, range.type
1500
+ FROM json_each(json_extract(selected.value, '$.after')) range
1501
+ ) range
1502
+ WHERE range.type != 'array'
1503
+ OR json_array_length(range.value) != 2
1504
+ OR COALESCE(json_type(range.value, '$[0]'), '') != 'integer'
1505
+ OR COALESCE(json_type(range.value, '$[1]'), '') != 'integer'
1506
+ OR json_extract(range.value, '$[0]') < 1
1507
+ OR (
1508
+ json_extract(range.value, '$[1]') != -1
1509
+ AND json_extract(range.value, '$[1]') < json_extract(range.value, '$[0]')
1510
+ )
1511
+ OR EXISTS (
1512
+ SELECT 1
1513
+ FROM json_each(
1514
+ CASE range.side
1515
+ WHEN 'before' THEN json_extract(selected.value, '$.before')
1516
+ ELSE json_extract(selected.value, '$.after')
1517
+ END
1518
+ ) previous
1519
+ WHERE previous.key = range.key - 1
1520
+ AND (
1521
+ json_extract(previous.value, '$[1]') = -1
1522
+ OR json_extract(range.value, '$[0]') <= json_extract(previous.value, '$[1]') + 1
1523
+ )
1524
+ )
1525
+ )
1049
1526
  )
1050
1527
  OR (
1051
- SELECT COUNT(*) FROM json_each(NEW.attrs, '$.__plurnk_curation.ids')
1528
+ SELECT COUNT(*) FROM json_each(NEW.attrs, '$.__plurnk_curation.targets')
1052
1529
  ) != (
1053
- SELECT COUNT(DISTINCT value) FROM json_each(NEW.attrs, '$.__plurnk_curation.ids')
1530
+ SELECT COUNT(DISTINCT json_extract(value, '$.id'))
1531
+ FROM json_each(NEW.attrs, '$.__plurnk_curation.targets')
1054
1532
  )
1055
1533
  OR EXISTS (
1056
1534
  SELECT 1
1057
- FROM json_each(NEW.attrs, '$.__plurnk_curation.ids') selected
1058
- LEFT JOIN log_entries target ON target.id = selected.value
1059
- WHERE target.id IS NULL OR target.worker_id != NEW.worker_id OR target.id = NEW.id
1535
+ FROM json_each(NEW.attrs, '$.__plurnk_curation.targets') selected
1536
+ LEFT JOIN log_entries target ON target.id = json_extract(selected.value, '$.id')
1537
+ WHERE target.id IS NULL
1538
+ OR target.worker_id != NEW.worker_id
1539
+ OR target.id = NEW.id
1540
+ OR json(target.folded) != json(json_extract(selected.value, '$.before'))
1060
1541
  )
1061
1542
  OR EXISTS (
1062
1543
  SELECT 1
@@ -1107,14 +1588,16 @@ BEGIN
1107
1588
  INSERT INTO log_curation_effects (
1108
1589
  operation_log_entry_id,
1109
1590
  target_log_entry_id,
1110
- expanded_before,
1591
+ folded_before,
1592
+ folded_after,
1111
1593
  tags_added,
1112
1594
  tags_removed
1113
1595
  )
1114
1596
  SELECT
1115
1597
  NEW.id,
1116
1598
  target.id,
1117
- target.expanded,
1599
+ json_extract(selected.value, '$.before'),
1600
+ json_extract(selected.value, '$.after'),
1118
1601
  COALESCE((
1119
1602
  SELECT json_group_array(ordered.tag)
1120
1603
  FROM (
@@ -1139,26 +1622,32 @@ BEGIN
1139
1622
  ORDER BY removal.value
1140
1623
  ) ordered
1141
1624
  ), '[]')
1142
- FROM json_each(NEW.attrs, '$.__plurnk_curation.ids') selected
1143
- JOIN log_entries target ON target.id = selected.value;
1625
+ FROM json_each(NEW.attrs, '$.__plurnk_curation.targets') selected
1626
+ JOIN log_entries target ON target.id = json_extract(selected.value, '$.id');
1144
1627
 
1145
1628
  UPDATE log_entries
1146
- SET expanded = json_extract(NEW.attrs, '$.__plurnk_curation.expanded')
1629
+ SET folded = (
1630
+ SELECT json_extract(selected.value, '$.after')
1631
+ FROM json_each(NEW.attrs, '$.__plurnk_curation.targets') selected
1632
+ WHERE json_extract(selected.value, '$.id') = log_entries.id
1633
+ )
1147
1634
  WHERE id IN (
1148
- SELECT value FROM json_each(NEW.attrs, '$.__plurnk_curation.ids')
1635
+ SELECT json_extract(value, '$.id')
1636
+ FROM json_each(NEW.attrs, '$.__plurnk_curation.targets')
1149
1637
  );
1150
1638
 
1151
1639
  DELETE FROM log_tags
1152
1640
  WHERE log_entry_id IN (
1153
- SELECT value FROM json_each(NEW.attrs, '$.__plurnk_curation.ids')
1641
+ SELECT json_extract(value, '$.id')
1642
+ FROM json_each(NEW.attrs, '$.__plurnk_curation.targets')
1154
1643
  )
1155
1644
  AND tag IN (
1156
1645
  SELECT value FROM json_each(NEW.attrs, '$.__plurnk_curation.remove')
1157
1646
  );
1158
1647
 
1159
1648
  INSERT OR IGNORE INTO log_tags (log_entry_id, tag)
1160
- SELECT selected.value, addition.value
1161
- FROM json_each(NEW.attrs, '$.__plurnk_curation.ids') selected
1649
+ SELECT json_extract(selected.value, '$.id'), addition.value
1650
+ FROM json_each(NEW.attrs, '$.__plurnk_curation.targets') selected
1162
1651
  CROSS JOIN json_each(NEW.attrs, '$.__plurnk_curation.add') addition;
1163
1652
 
1164
1653
  UPDATE log_entries
@@ -1168,18 +1657,18 @@ END;
1168
1657
 
1169
1658
  -- Column-scoped immutability: the original action's identity and target never
1170
1659
  -- change; the proposal lifecycle is allowed to mutate state, outcome,
1171
- -- status_rx, rx, expanded. Keep attrs separate so the curation trigger's one
1660
+ -- status_rx, rx, folded. Keep attrs separate so the curation trigger's one
1172
1661
  -- private-payload removal cannot exempt changes to any other core column.
1173
1662
  CREATE TRIGGER IF NOT EXISTS log_entries_immutable_core
1174
1663
  BEFORE UPDATE OF
1175
- worker_id, loop_id, turn_id, sequence, at, origin, source, model_call_id,
1176
- op, suffix, signal,
1664
+ worker_id, loop_id, turn_id, sequence, at, origin, source, inherited_history, model_call_id,
1665
+ op, delimiter, signal,
1177
1666
  scheme, username, password, hostname,
1178
1667
  port, pathname, query, fragment,
1179
1668
  lineMarker, tx, mimetype_tx, mimetype_rx
1180
1669
  ON log_entries
1181
1670
  BEGIN
1182
- SELECT RAISE(ABORT, 'log_entries core fields are immutable; only state/outcome/status_rx/rx/expanded may change');
1671
+ SELECT RAISE(ABORT, 'log_entries core fields are immutable; only state/outcome/status_rx/rx/folded may change');
1183
1672
  END;
1184
1673
 
1185
1674
  CREATE TRIGGER IF NOT EXISTS log_entries_immutable_attrs
@@ -1201,72 +1690,168 @@ BEGIN
1201
1690
  SELECT RAISE(ABORT, 'log_entries ambient event identity may only be assigned once');
1202
1691
  END;
1203
1692
 
1204
- -- Immediate successful shared EDITs append their occurrence in the same SQL
1205
- -- statement that persists the receipt. Rows already carrying an event id are
1206
- -- observer/fork history and can never republish themselves.
1693
+ -- One relational projection owns activity eligibility, audience, and snapshot
1694
+ -- shape for both immediately-final and proposal-settlement paths.
1695
+ CREATE VIEW IF NOT EXISTS ambient_activity_candidates AS
1696
+ SELECT *
1697
+ FROM (
1698
+ SELECT w.workspace_id,
1699
+ le.worker_id AS producer_worker_id,
1700
+ CASE
1701
+ WHEN t.producer = '_plurnk' AND t.kind = 'operation' THEN NULL
1702
+ ELSE w.parent_worker_id
1703
+ END AS target_parent_worker_id,
1704
+ CASE
1705
+ WHEN le.state = 'resolved'
1706
+ AND le.status_rx BETWEEN 200 AND 399
1707
+ AND le.status_rx != 304
1708
+ AND (
1709
+ (
1710
+ (
1711
+ le.op IN ('EDIT', 'KILL')
1712
+ OR (
1713
+ le.op = 'SEND'
1714
+ AND json_valid(le.signal)
1715
+ AND json_extract(le.signal, '$') = 410
1716
+ )
1717
+ )
1718
+ AND le.scheme = 'worker'
1719
+ AND le.hostname IS NULL
1720
+ AND le.pathname IS NOT NULL
1721
+ )
1722
+ OR (
1723
+ le.op IN ('COPY', 'MOVE')
1724
+ AND json_valid(le.rx)
1725
+ AND EXISTS (
1726
+ SELECT 1 FROM json_each(le.rx, '$.effects') effect
1727
+ WHERE json_type(effect.value) = 'object'
1728
+ AND substr(CAST(json_extract(effect.value, '$.target') AS TEXT), 1, 10) = 'worker:///'
1729
+ )
1730
+ )
1731
+ )
1732
+ THEN 1 ELSE 0
1733
+ END AS workspace_broadcast,
1734
+ le.id AS source_record_id,
1735
+ le.at,
1736
+ le.source,
1737
+ le.op,
1738
+ le.delimiter,
1739
+ le.signal,
1740
+ le.scheme,
1741
+ le.username,
1742
+ le.password,
1743
+ le.hostname,
1744
+ le.port,
1745
+ le.pathname,
1746
+ le.query,
1747
+ le.fragment,
1748
+ le.lineMarker AS line_marker,
1749
+ le.tx,
1750
+ le.mimetype_tx,
1751
+ le.rx,
1752
+ le.mimetype_rx,
1753
+ le.status_rx,
1754
+ le.state,
1755
+ le.outcome,
1756
+ json_remove(le.attrs, '$.__plurnk_curation') AS attrs,
1757
+ COALESCE((
1758
+ SELECT json_group_array(ordered.tag)
1759
+ FROM (
1760
+ SELECT tag FROM log_tags WHERE log_entry_id = le.id ORDER BY tag
1761
+ ) ordered
1762
+ ), '[]') AS tags
1763
+ FROM log_entries le
1764
+ JOIN workers w ON w.id = le.worker_id
1765
+ JOIN loops l ON l.id = le.loop_id
1766
+ JOIN turns t ON t.id = le.turn_id
1767
+ WHERE le.ambient_event_id IS NULL
1768
+ AND le.inherited_history = 0
1769
+ AND le.op IS NOT NULL
1770
+ AND le.state != 'proposed'
1771
+ AND NOT (
1772
+ le.op = 'SEND'
1773
+ AND le.scheme IS NULL
1774
+ AND l.status IN (200, 413, 429, 499, 500, 504, 508)
1775
+ )
1776
+ ) candidate
1777
+ WHERE target_parent_worker_id IS NOT NULL OR workspace_broadcast = 1;
1778
+
1779
+ -- Every final op-bearing child row is parent activity. A successful operation
1780
+ -- whose landed effects touch worker:/// additionally acquires the workspace
1781
+ -- audience. Observer rows and copied fork history cannot republish themselves.
1207
1782
  CREATE TRIGGER IF NOT EXISTS log_entries_append_ambient_event_insert
1208
1783
  AFTER INSERT ON log_entries
1209
- WHEN NEW.ambient_event_id IS NULL
1210
- AND NEW.op = 'EDIT'
1211
- AND NEW.state = 'resolved'
1212
- AND NEW.status_rx IN (200, 201)
1213
- AND (NEW.scheme IS NULL OR NEW.scheme != 'plurnk')
1214
- AND (NEW.scheme IS NULL OR NEW.scheme != 'worker' OR NEW.hostname IS NULL OR NEW.hostname = 'plurnk')
1215
- AND (
1216
- NEW.origin != 'plurnk'
1217
- OR EXISTS (SELECT 1 FROM workers w WHERE w.id = NEW.worker_id AND w.name = 'plurnk')
1218
- )
1784
+ WHEN NEW.state != 'proposed'
1219
1785
  BEGIN
1220
1786
  INSERT INTO ambient_events (
1221
- workspace_id, producer_worker_id, kind, source_record_id, source,
1222
- op, scheme, hostname, pathname, rx, attrs, tags, status_rx
1787
+ workspace_id, producer_worker_id, target_parent_worker_id,
1788
+ workspace_broadcast, kind, source_record_id, at, source,
1789
+ op, delimiter, signal,
1790
+ scheme, username, password, hostname, port, pathname, query, fragment,
1791
+ line_marker, tx, mimetype_tx, rx, mimetype_rx, status_rx,
1792
+ state, outcome, attrs, tags
1223
1793
  )
1224
- SELECT w.workspace_id, NEW.worker_id, 'edit', NEW.id, NEW.source,
1225
- 'EDIT', NEW.scheme, NEW.hostname, NEW.pathname, NEW.rx, NEW.attrs,
1226
- COALESCE((
1227
- SELECT json_group_array(ordered.tag)
1228
- FROM (
1229
- SELECT tag FROM log_tags WHERE log_entry_id = NEW.id ORDER BY tag
1230
- ) ordered
1231
- ), '[]'),
1232
- NEW.status_rx
1233
- FROM workers w
1234
- WHERE w.id = NEW.worker_id;
1235
- UPDATE log_entries SET ambient_event_id = last_insert_rowid() WHERE id = NEW.id;
1794
+ SELECT workspace_id, producer_worker_id, target_parent_worker_id,
1795
+ workspace_broadcast, 'activity', source_record_id, at, source,
1796
+ op, delimiter, signal,
1797
+ scheme, username, password, hostname, port, pathname, query, fragment,
1798
+ line_marker, tx, mimetype_tx, rx, mimetype_rx, status_rx,
1799
+ state, outcome, attrs, tags
1800
+ FROM ambient_activity_candidates
1801
+ WHERE source_record_id = NEW.id;
1802
+
1803
+ UPDATE log_entries
1804
+ SET ambient_event_id = (
1805
+ SELECT id FROM ambient_events
1806
+ WHERE producer_worker_id = NEW.worker_id
1807
+ AND kind = 'activity'
1808
+ AND source_record_id = NEW.id
1809
+ )
1810
+ WHERE id = NEW.id
1811
+ AND EXISTS (
1812
+ SELECT 1 FROM ambient_events
1813
+ WHERE producer_worker_id = NEW.worker_id
1814
+ AND kind = 'activity'
1815
+ AND source_record_id = NEW.id
1816
+ );
1236
1817
  END;
1237
1818
 
1238
- -- A proposed EDIT becomes an occurrence only when its one lifecycle transition
1239
- -- resolves successfully. Rejection, cancellation, and failed application publish nothing.
1819
+ -- A proposed operation becomes activity only when its lifecycle settles.
1240
1820
  CREATE TRIGGER IF NOT EXISTS log_entries_append_ambient_event_resolve
1241
- AFTER UPDATE OF state, status_rx ON log_entries
1242
- WHEN OLD.state = 'proposed'
1243
- AND NEW.state = 'resolved'
1244
- AND NEW.ambient_event_id IS NULL
1245
- AND NEW.op = 'EDIT'
1246
- AND NEW.status_rx IN (200, 201)
1247
- AND (NEW.scheme IS NULL OR NEW.scheme != 'plurnk')
1248
- AND (NEW.scheme IS NULL OR NEW.scheme != 'worker' OR NEW.hostname IS NULL OR NEW.hostname = 'plurnk')
1249
- AND (
1250
- NEW.origin != 'plurnk'
1251
- OR EXISTS (SELECT 1 FROM workers w WHERE w.id = NEW.worker_id AND w.name = 'plurnk')
1252
- )
1821
+ AFTER UPDATE OF state, status_rx, rx, outcome ON log_entries
1822
+ WHEN OLD.state = 'proposed' AND NEW.state != 'proposed'
1253
1823
  BEGIN
1254
1824
  INSERT INTO ambient_events (
1255
- workspace_id, producer_worker_id, kind, source_record_id, source,
1256
- op, scheme, hostname, pathname, rx, attrs, tags, status_rx
1825
+ workspace_id, producer_worker_id, target_parent_worker_id,
1826
+ workspace_broadcast, kind, source_record_id, at, source,
1827
+ op, delimiter, signal,
1828
+ scheme, username, password, hostname, port, pathname, query, fragment,
1829
+ line_marker, tx, mimetype_tx, rx, mimetype_rx, status_rx,
1830
+ state, outcome, attrs, tags
1257
1831
  )
1258
- SELECT w.workspace_id, NEW.worker_id, 'edit', NEW.id, NEW.source,
1259
- 'EDIT', NEW.scheme, NEW.hostname, NEW.pathname, NEW.rx, NEW.attrs,
1260
- COALESCE((
1261
- SELECT json_group_array(ordered.tag)
1262
- FROM (
1263
- SELECT tag FROM log_tags WHERE log_entry_id = NEW.id ORDER BY tag
1264
- ) ordered
1265
- ), '[]'),
1266
- NEW.status_rx
1267
- FROM workers w
1268
- WHERE w.id = NEW.worker_id;
1269
- UPDATE log_entries SET ambient_event_id = last_insert_rowid() WHERE id = NEW.id;
1832
+ SELECT workspace_id, producer_worker_id, target_parent_worker_id,
1833
+ workspace_broadcast, 'activity', source_record_id, at, source,
1834
+ op, delimiter, signal,
1835
+ scheme, username, password, hostname, port, pathname, query, fragment,
1836
+ line_marker, tx, mimetype_tx, rx, mimetype_rx, status_rx,
1837
+ state, outcome, attrs, tags
1838
+ FROM ambient_activity_candidates
1839
+ WHERE source_record_id = NEW.id;
1840
+
1841
+ UPDATE log_entries
1842
+ SET ambient_event_id = (
1843
+ SELECT id FROM ambient_events
1844
+ WHERE producer_worker_id = NEW.worker_id
1845
+ AND kind = 'activity'
1846
+ AND source_record_id = NEW.id
1847
+ )
1848
+ WHERE id = NEW.id
1849
+ AND EXISTS (
1850
+ SELECT 1 FROM ambient_events
1851
+ WHERE producer_worker_id = NEW.worker_id
1852
+ AND kind = 'activity'
1853
+ AND source_record_id = NEW.id
1854
+ );
1270
1855
  END;
1271
1856
 
1272
1857
  -- {§client-interactions}: the durable discoverable half of a client-owned
@@ -1293,7 +1878,8 @@ CREATE INDEX IF NOT EXISTS client_interactions_worker_id_id
1293
1878
  -- Durable subscription lifecycle per SPEC {§subscriptions}. The row records what
1294
1879
  -- the worker holds and routes cancellation to a separate process-local callable;
1295
1880
  -- it never serializes that callable. Closed rows persist for forensics; partial
1296
- -- unique index enforces one active subscription per (worker, entry).
1881
+ -- unique index enforces one active subscription per entry. The composite
1882
+ -- foreign key makes that entry part of the subscribing Worker's own space.
1297
1883
  CREATE TABLE IF NOT EXISTS subscriptions (
1298
1884
  id INTEGER NOT NULL PRIMARY KEY,
1299
1885
  version INTEGER NOT NULL DEFAULT 0 CHECK (version >= 0),
@@ -1312,14 +1898,15 @@ CREATE TABLE IF NOT EXISTS subscriptions (
1312
1898
  closed_at TEXT,
1313
1899
  close_status INTEGER CHECK (close_status IS NULL OR (close_status BETWEEN 100 AND 599)),
1314
1900
  close_result TEXT CHECK (close_result IS NULL OR json_valid(close_result)),
1315
- CHECK ((closed_at IS NULL AND close_status IS NULL)
1316
- OR (closed_at IS NOT NULL AND close_status IS NOT NULL)),
1317
- FOREIGN KEY (worker_id) REFERENCES workers(id) ON DELETE CASCADE,
1318
- FOREIGN KEY (entry_id) REFERENCES entries(id) ON DELETE CASCADE
1901
+ channel_results TEXT CHECK (channel_results IS NULL OR json_valid(channel_results)),
1902
+ CHECK ((closed_at IS NULL AND close_status IS NULL AND close_result IS NULL AND channel_results IS NULL)
1903
+ OR (closed_at IS NOT NULL AND close_status IS NOT NULL AND close_result IS NOT NULL AND channel_results IS NOT NULL)),
1904
+ FOREIGN KEY (worker_id) REFERENCES workers(id) ON DELETE CASCADE,
1905
+ FOREIGN KEY (entry_id, worker_id) REFERENCES entries(id, owner_id) ON DELETE CASCADE
1319
1906
  ) STRICT;
1320
1907
 
1321
1908
  CREATE UNIQUE INDEX IF NOT EXISTS subscriptions_active_one_per_entry
1322
- ON subscriptions (worker_id, entry_id)
1909
+ ON subscriptions (entry_id)
1323
1910
  WHERE closed_at IS NULL;
1324
1911
 
1325
1912
  CREATE INDEX IF NOT EXISTS subscriptions_scheme_active
@@ -1330,13 +1917,26 @@ CREATE INDEX IF NOT EXISTS subscriptions_opened_at ON subscriptions (opened_at);
1330
1917
 
1331
1918
  CREATE TRIGGER IF NOT EXISTS subscriptions_result_contract_insert
1332
1919
  BEFORE INSERT ON subscriptions
1920
+ WHEN NEW.closed_at IS NOT NULL
1921
+ OR NEW.close_status IS NOT NULL
1922
+ OR NEW.close_result IS NOT NULL
1923
+ OR NEW.channel_results IS NOT NULL
1924
+ BEGIN
1925
+ SELECT RAISE(ABORT, 'subscription must open before it can settle');
1926
+ END;
1927
+
1928
+ CREATE TRIGGER IF NOT EXISTS subscriptions_result_contract_update
1929
+ BEFORE UPDATE OF closed_at, close_status, close_result, channel_results ON subscriptions
1333
1930
  WHEN NOT (
1334
- (NEW.closed_at IS NULL AND NEW.close_status IS NULL AND NEW.close_result IS NULL)
1931
+ (NEW.closed_at IS NULL AND NEW.close_status IS NULL AND NEW.close_result IS NULL AND NEW.channel_results IS NULL)
1335
1932
  OR (
1336
1933
  NEW.closed_at IS NOT NULL
1337
1934
  AND NEW.close_status IS NOT NULL
1338
1935
  AND NEW.close_result IS NOT NULL
1936
+ AND NEW.channel_results IS NOT NULL
1339
1937
  AND json_valid(NEW.close_result)
1938
+ AND json_valid(NEW.channel_results)
1939
+ AND json_type(NEW.channel_results) = 'object'
1340
1940
  AND json_type(NEW.close_result, '$.status') = 'integer'
1341
1941
  AND json_extract(NEW.close_result, '$.status') = NEW.close_status
1342
1942
  AND (
@@ -1353,35 +1953,142 @@ BEGIN
1353
1953
  SELECT RAISE(ABORT, 'subscription terminal result violates the operation-result contract');
1354
1954
  END;
1355
1955
 
1356
- CREATE TRIGGER IF NOT EXISTS subscriptions_result_contract_update
1357
- BEFORE UPDATE OF closed_at, close_status, close_result ON subscriptions
1358
- WHEN NOT (
1359
- (NEW.closed_at IS NULL AND NEW.close_status IS NULL AND NEW.close_result IS NULL)
1360
- OR (
1361
- NEW.closed_at IS NOT NULL
1362
- AND NEW.close_status IS NOT NULL
1363
- AND NEW.close_result IS NOT NULL
1364
- AND json_valid(NEW.close_result)
1365
- AND json_type(NEW.close_result, '$.status') = 'integer'
1366
- AND json_extract(NEW.close_result, '$.status') = NEW.close_status
1367
- AND (
1368
- (NEW.close_status < 400 AND json_type(NEW.close_result, '$.problem') IS NULL)
1369
- OR (
1370
- NEW.close_status >= 400
1371
- AND json_type(NEW.close_result, '$.problem') = 'object'
1372
- AND json_extract(NEW.close_result, '$.problem.status') = NEW.close_status
1373
- )
1374
- )
1956
+ -- A settlement may override the universal result for exact named channels.
1957
+ -- Unknown channels and malformed results fail before the subscription or any
1958
+ -- representation changes. The JS boundary additionally rejects projection
1959
+ -- fields that are structurally unavailable in ChannelProducerResult.
1960
+ CREATE TRIGGER IF NOT EXISTS subscriptions_channel_results_contract
1961
+ BEFORE UPDATE OF closed_at, close_status, close_result, channel_results ON subscriptions
1962
+ WHEN NEW.closed_at IS NOT NULL AND (
1963
+ EXISTS (
1964
+ SELECT 1
1965
+ FROM json_each(NEW.channel_results) AS channel_result
1966
+ LEFT JOIN entry_channels ec
1967
+ ON ec.entry_id = NEW.entry_id AND ec.name = channel_result.key
1968
+ WHERE ec.name IS NULL
1969
+ OR json_type(channel_result.value) != 'object'
1970
+ OR json_type(channel_result.value, '$.status') != 'integer'
1971
+ OR json_extract(channel_result.value, '$.status') NOT BETWEEN 200 AND 599
1972
+ OR json_extract(channel_result.value, '$.status') = 202
1973
+ OR (
1974
+ json_extract(channel_result.value, '$.status') < 400
1975
+ AND json_type(channel_result.value, '$.problem') IS NOT NULL
1976
+ )
1977
+ OR (
1978
+ json_extract(channel_result.value, '$.status') >= 400
1979
+ AND (
1980
+ json_type(channel_result.value, '$.problem') != 'object'
1981
+ OR json_extract(channel_result.value, '$.problem.status')
1982
+ != json_extract(channel_result.value, '$.status')
1983
+ )
1984
+ )
1375
1985
  )
1376
1986
  )
1377
1987
  BEGIN
1378
- SELECT RAISE(ABORT, 'subscription terminal result violates the operation-result contract');
1988
+ SELECT RAISE(ABORT, 'subscription channel result violates the channel producer contract');
1989
+ END;
1990
+
1991
+ -- Subscription settlement is the single atomic transition that closes the
1992
+ -- lifecycle and installs current terminal producer evidence on every channel.
1993
+ -- Overrides are exact; all other channels inherit the universal result.
1994
+ CREATE TRIGGER IF NOT EXISTS subscriptions_settle_channels
1995
+ AFTER UPDATE OF closed_at, close_status, close_result, channel_results ON subscriptions
1996
+ WHEN OLD.closed_at IS NULL AND NEW.closed_at IS NOT NULL
1997
+ BEGIN
1998
+ UPDATE entry_channels
1999
+ SET producer_result = COALESCE(
2000
+ (
2001
+ SELECT json(channel_result.value)
2002
+ FROM json_each(NEW.channel_results) AS channel_result
2003
+ WHERE channel_result.key = entry_channels.name
2004
+ ),
2005
+ NEW.close_result
2006
+ ),
2007
+ state = CASE WHEN json_extract(
2008
+ COALESCE(
2009
+ (
2010
+ SELECT json(channel_result.value)
2011
+ FROM json_each(NEW.channel_results) AS channel_result
2012
+ WHERE channel_result.key = entry_channels.name
2013
+ ),
2014
+ NEW.close_result
2015
+ ),
2016
+ '$.status'
2017
+ ) >= 400 THEN 'errored' ELSE 'closed' END
2018
+ WHERE entry_id = NEW.entry_id;
2019
+ END;
2020
+
2021
+ -- One durable publication cursor per selected subscription channel. Log rows
2022
+ -- are curated model context and therefore cannot own this lifecycle fact.
2023
+ -- The row survives KILLing any generated observation and disappears only with
2024
+ -- its subscription. {§exec-stream}
2025
+ CREATE TABLE IF NOT EXISTS subscription_publications (
2026
+ id INTEGER NOT NULL PRIMARY KEY,
2027
+ version INTEGER NOT NULL DEFAULT 0 CHECK (version >= 0),
2028
+ subscription_id INTEGER NOT NULL,
2029
+ channel TEXT NOT NULL CHECK (length(channel) > 0),
2030
+ published_end INTEGER NOT NULL DEFAULT 0 CHECK (published_end >= 0),
2031
+ terminal_published INTEGER NOT NULL DEFAULT 0 CHECK (terminal_published IN (0, 1)),
2032
+ UNIQUE (subscription_id, channel),
2033
+ FOREIGN KEY (subscription_id) REFERENCES subscriptions(id) ON DELETE CASCADE
2034
+ ) STRICT;
2035
+
2036
+ CREATE INDEX IF NOT EXISTS subscription_publications_pending
2037
+ ON subscription_publications (subscription_id, terminal_published);
2038
+
2039
+ CREATE TRIGGER IF NOT EXISTS subscriptions_seed_publications
2040
+ AFTER INSERT ON subscriptions
2041
+ BEGIN
2042
+ INSERT INTO subscription_publications (subscription_id, channel)
2043
+ SELECT NEW.id, ec.name
2044
+ FROM entry_channels ec
2045
+ WHERE ec.entry_id = NEW.entry_id
2046
+ AND (NEW.published_channel IS NULL OR ec.name = NEW.published_channel);
2047
+
2048
+ SELECT CASE WHEN NOT EXISTS (
2049
+ SELECT 1 FROM subscription_publications WHERE subscription_id = NEW.id
2050
+ ) THEN RAISE(ABORT, 'subscription has no publishable channel') END;
2051
+ END;
2052
+
2053
+ -- The generated READ and its cursor transition are one SQLite statement. A
2054
+ -- later log KILL removes evidence from active context without rewinding the
2055
+ -- subscription/channel publication state. {§exec-stream}
2056
+ CREATE TRIGGER IF NOT EXISTS log_entries_advance_subscription_publication
2057
+ AFTER INSERT ON log_entries
2058
+ WHEN NEW.subscription_publication_id IS NOT NULL
2059
+ BEGIN
2060
+ SELECT CASE WHEN
2061
+ NEW.origin != '_plurnk'
2062
+ OR NEW.op != 'READ'
2063
+ OR json_type(NEW.attrs, '$.streamEnd') != 'integer'
2064
+ OR json_extract(NEW.attrs, '$.streamEnd') < 0
2065
+ OR json_type(NEW.attrs, '$.terminal') NOT IN ('true', 'false')
2066
+ THEN RAISE(ABORT, 'subscription publication requires one canonical stream observation') END;
2067
+
2068
+ UPDATE subscription_publications
2069
+ SET published_end = json_extract(NEW.attrs, '$.streamEnd'),
2070
+ terminal_published = json_extract(NEW.attrs, '$.terminal'),
2071
+ version = version + 1
2072
+ WHERE id = NEW.subscription_publication_id
2073
+ AND terminal_published = 0
2074
+ AND (
2075
+ (json_extract(NEW.attrs, '$.terminal') = 0
2076
+ AND json_extract(NEW.attrs, '$.streamEnd') > published_end)
2077
+ OR
2078
+ (json_extract(NEW.attrs, '$.terminal') = 1
2079
+ AND json_extract(NEW.attrs, '$.streamEnd') >= published_end)
2080
+ );
2081
+
2082
+ SELECT CASE WHEN changes() != 1
2083
+ THEN RAISE(ABORT, 'subscription publication transition is stale or already terminal')
2084
+ END;
1379
2085
  END;
1380
2086
 
1381
2087
  -- (worker_watermarks removed — {§env-delta} is now pull-from-log, no per-worker snapshot.)
1382
2088
 
1383
2089
  -- workspace_constraints
1384
- -- SPEC {§membership} constraint overlay — the client's supersede over git membership.
2090
+ -- SPEC {§membership} constraint overlay — explicit policy and the inspectable
2091
+ -- exact pick produced by accepted file creation share one representation.
1385
2092
  -- Per (workspace, effect, glob/target): `pick` (members git misses, resolved by a
1386
2093
  -- targeted client-dictated scan), `hide` (drop git-tracked matches), `view` (member
1387
2094
  -- for read; File.edit rejects the write). git-absent, `pick` rows are the sole substrate
@@ -1390,6 +2097,8 @@ CREATE TABLE IF NOT EXISTS workspace_constraints (
1390
2097
  workspace_id INTEGER NOT NULL,
1391
2098
  effect TEXT NOT NULL CHECK (effect IN ('pick', 'hide', 'view')),
1392
2099
  glob TEXT NOT NULL,
2100
+ source TEXT NOT NULL CHECK (source IN ('explicit', 'create')),
2101
+ CHECK (source = 'explicit' OR effect = 'pick'),
1393
2102
  PRIMARY KEY (workspace_id, effect, glob),
1394
2103
  FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE
1395
2104
  ) STRICT, WITHOUT ROWID;