@plurnk/plurnk-service 1.6.1 → 1.8.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 (310) hide show
  1. package/.env.defaults +29 -42
  2. package/INSTALL.md +49 -10
  3. package/README.md +11 -1
  4. package/SPEC.md +702 -287
  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 +2 -5
  14. package/dist/Paths.d.ts.map +1 -1
  15. package/dist/Paths.js +9 -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.map +1 -1
  25. package/dist/core/ChannelWrite.js.map +1 -1
  26. package/dist/core/ChannelWrite.sql +2 -1
  27. package/dist/core/ClientInteractions.d.ts +19 -0
  28. package/dist/core/ClientInteractions.d.ts.map +1 -0
  29. package/dist/core/ClientInteractions.js +124 -0
  30. package/dist/core/ClientInteractions.js.map +1 -0
  31. package/dist/core/ClientInteractions.sql +34 -0
  32. package/dist/core/CoreSchemeServices.d.ts +8 -2
  33. package/dist/core/CoreSchemeServices.d.ts.map +1 -1
  34. package/dist/core/CoreSchemeServices.js +7 -1
  35. package/dist/core/CoreSchemeServices.js.map +1 -1
  36. package/dist/core/Dispatcher.d.ts +9 -6
  37. package/dist/core/Dispatcher.d.ts.map +1 -1
  38. package/dist/core/Dispatcher.js +74 -125
  39. package/dist/core/Dispatcher.js.map +1 -1
  40. package/dist/core/Engine.d.ts +26 -9
  41. package/dist/core/Engine.d.ts.map +1 -1
  42. package/dist/core/Engine.js +125 -44
  43. package/dist/core/Engine.js.map +1 -1
  44. package/dist/core/Engine.sql +112 -107
  45. package/dist/core/ExecutorRegistry.d.ts +19 -5
  46. package/dist/core/ExecutorRegistry.d.ts.map +1 -1
  47. package/dist/core/ExecutorRegistry.js +144 -18
  48. package/dist/core/ExecutorRegistry.js.map +1 -1
  49. package/dist/core/HostPaths.d.ts +29 -0
  50. package/dist/core/HostPaths.d.ts.map +1 -0
  51. package/dist/core/HostPaths.js +74 -0
  52. package/dist/core/HostPaths.js.map +1 -0
  53. package/dist/core/LegacyHome.d.ts +10 -0
  54. package/dist/core/LegacyHome.d.ts.map +1 -0
  55. package/dist/core/LegacyHome.js +170 -0
  56. package/dist/core/LegacyHome.js.map +1 -0
  57. package/dist/core/LogBody.d.ts +1 -1
  58. package/dist/core/LogBody.d.ts.map +1 -1
  59. package/dist/core/LogBody.js +2 -2
  60. package/dist/core/LogBody.js.map +1 -1
  61. package/dist/core/LogEntryProjection.js +1 -1
  62. package/dist/core/LogEntryProjection.js.map +1 -1
  63. package/dist/core/LogVisibility.d.ts +28 -0
  64. package/dist/core/LogVisibility.d.ts.map +1 -0
  65. package/dist/core/LogVisibility.js +215 -0
  66. package/dist/core/LogVisibility.js.map +1 -0
  67. package/dist/core/LoopLifecycle.d.ts +1 -0
  68. package/dist/core/LoopLifecycle.d.ts.map +1 -1
  69. package/dist/core/LoopLifecycle.js +8 -0
  70. package/dist/core/LoopLifecycle.js.map +1 -1
  71. package/dist/core/LoopLifecycle.sql +10 -0
  72. package/dist/core/OperatorConfig.d.ts +6 -0
  73. package/dist/core/OperatorConfig.d.ts.map +1 -0
  74. package/dist/core/OperatorConfig.js +92 -0
  75. package/dist/core/OperatorConfig.js.map +1 -0
  76. package/dist/core/OverflowTurn.d.ts +11 -0
  77. package/dist/core/OverflowTurn.d.ts.map +1 -0
  78. package/dist/core/OverflowTurn.js +77 -0
  79. package/dist/core/OverflowTurn.js.map +1 -0
  80. package/dist/core/Owner.js +2 -2
  81. package/dist/core/Owner.js.map +1 -1
  82. package/dist/core/PacketBuilder.d.ts +3 -15
  83. package/dist/core/PacketBuilder.d.ts.map +1 -1
  84. package/dist/core/PacketBuilder.js +52 -113
  85. package/dist/core/PacketBuilder.js.map +1 -1
  86. package/dist/core/ProblemLog.js +1 -1
  87. package/dist/core/ProblemLog.js.map +1 -1
  88. package/dist/core/ProposalLifecycle.d.ts +3 -1
  89. package/dist/core/ProposalLifecycle.d.ts.map +1 -1
  90. package/dist/core/ProposalLifecycle.js +9 -26
  91. package/dist/core/ProposalLifecycle.js.map +1 -1
  92. package/dist/core/ProviderInstantiate.d.ts +9 -5
  93. package/dist/core/ProviderInstantiate.d.ts.map +1 -1
  94. package/dist/core/ProviderInstantiate.js +99 -104
  95. package/dist/core/ProviderInstantiate.js.map +1 -1
  96. package/dist/core/ResourceMutations.d.ts +2 -2
  97. package/dist/core/ResourceMutations.d.ts.map +1 -1
  98. package/dist/core/ResourceMutations.js +13 -11
  99. package/dist/core/ResourceMutations.js.map +1 -1
  100. package/dist/core/SchemeRegistry.d.ts +18 -11
  101. package/dist/core/SchemeRegistry.d.ts.map +1 -1
  102. package/dist/core/SchemeRegistry.js +201 -53
  103. package/dist/core/SchemeRegistry.js.map +1 -1
  104. package/dist/core/ToolResources.d.ts +19 -0
  105. package/dist/core/ToolResources.d.ts.map +1 -0
  106. package/dist/core/ToolResources.js +122 -0
  107. package/dist/core/ToolResources.js.map +1 -0
  108. package/dist/core/Turn.d.ts +27 -0
  109. package/dist/core/Turn.d.ts.map +1 -0
  110. package/dist/core/Turn.js +43 -0
  111. package/dist/core/Turn.js.map +1 -0
  112. package/dist/core/Turn.sql +72 -0
  113. package/dist/core/TurnOps.d.ts +7 -0
  114. package/dist/core/TurnOps.d.ts.map +1 -0
  115. package/dist/core/TurnOps.js +63 -0
  116. package/dist/core/TurnOps.js.map +1 -0
  117. package/dist/core/TurnRunner.d.ts +15 -5
  118. package/dist/core/TurnRunner.d.ts.map +1 -1
  119. package/dist/core/TurnRunner.js +1050 -900
  120. package/dist/core/TurnRunner.js.map +1 -1
  121. package/dist/core/WorkerName.d.ts +1 -1
  122. package/dist/core/WorkerName.d.ts.map +1 -1
  123. package/dist/core/WorkspaceGate.d.ts +1 -0
  124. package/dist/core/WorkspaceGate.d.ts.map +1 -1
  125. package/dist/core/WorkspaceGate.js +39 -0
  126. package/dist/core/WorkspaceGate.js.map +1 -1
  127. package/dist/core/caps/CoreInteractionCaps.d.ts +9 -0
  128. package/dist/core/caps/CoreInteractionCaps.d.ts.map +1 -0
  129. package/dist/core/caps/CoreInteractionCaps.js +14 -0
  130. package/dist/core/caps/CoreInteractionCaps.js.map +1 -0
  131. package/dist/core/caps/SchemeCtxImpl.d.ts +2 -1
  132. package/dist/core/caps/SchemeCtxImpl.d.ts.map +1 -1
  133. package/dist/core/caps/SchemeCtxImpl.js +3 -0
  134. package/dist/core/caps/SchemeCtxImpl.js.map +1 -1
  135. package/dist/core/env-defaults.d.ts.map +1 -1
  136. package/dist/core/env-defaults.js +7 -5
  137. package/dist/core/env-defaults.js.map +1 -1
  138. package/dist/core/fork.d.ts.map +1 -1
  139. package/dist/core/fork.js +15 -4
  140. package/dist/core/fork.js.map +1 -1
  141. package/dist/core/fork.sql +57 -27
  142. package/dist/core/git-env.d.ts +0 -1
  143. package/dist/core/git-env.d.ts.map +1 -1
  144. package/dist/core/git-env.js +0 -3
  145. package/dist/core/git-env.js.map +1 -1
  146. package/dist/core/git-membership.d.ts.map +1 -1
  147. package/dist/core/git-membership.js +10 -26
  148. package/dist/core/git-membership.js.map +1 -1
  149. package/dist/core/git-state.d.ts.map +1 -1
  150. package/dist/core/git-state.js +13 -14
  151. package/dist/core/git-state.js.map +1 -1
  152. package/dist/core/operation-target-groups.d.ts +3 -0
  153. package/dist/core/operation-target-groups.d.ts.map +1 -0
  154. package/dist/core/operation-target-groups.js +53 -0
  155. package/dist/core/operation-target-groups.js.map +1 -0
  156. package/dist/core/packet-inject.d.ts +0 -1
  157. package/dist/core/packet-inject.d.ts.map +1 -1
  158. package/dist/core/packet-inject.js +6 -13
  159. package/dist/core/packet-inject.js.map +1 -1
  160. package/dist/core/packet-wire.d.ts +4 -0
  161. package/dist/core/packet-wire.d.ts.map +1 -1
  162. package/dist/core/packet-wire.js +208 -44
  163. package/dist/core/packet-wire.js.map +1 -1
  164. package/dist/core/plurnk-uri.d.ts +1 -1
  165. package/dist/core/plurnk-uri.d.ts.map +1 -1
  166. package/dist/core/plurnk-uri.js +3 -3
  167. package/dist/core/plurnk-uri.js.map +1 -1
  168. package/dist/core/scheme-types.d.ts +2 -1
  169. package/dist/core/scheme-types.d.ts.map +1 -1
  170. package/dist/core/scheme-types.js.map +1 -1
  171. package/dist/core/teaching.d.ts.map +1 -1
  172. package/dist/core/teaching.js +4 -5
  173. package/dist/core/teaching.js.map +1 -1
  174. package/dist/core/worker-settings.d.ts +9 -0
  175. package/dist/core/worker-settings.d.ts.map +1 -0
  176. package/dist/core/worker-settings.js +25 -0
  177. package/dist/core/worker-settings.js.map +1 -0
  178. package/dist/core/workspace-settings.d.ts +0 -11
  179. package/dist/core/workspace-settings.d.ts.map +1 -1
  180. package/dist/core/workspace-settings.js +3 -47
  181. package/dist/core/workspace-settings.js.map +1 -1
  182. package/dist/digest/Digest.d.ts.map +1 -1
  183. package/dist/digest/Digest.js +164 -92
  184. package/dist/digest/Digest.js.map +1 -1
  185. package/dist/digest/digest.sql +8 -2
  186. package/dist/observe/genai.d.ts +9 -0
  187. package/dist/observe/genai.d.ts.map +1 -0
  188. package/dist/observe/genai.js +31 -0
  189. package/dist/observe/genai.js.map +1 -0
  190. package/dist/observe/spans.d.ts +3 -3
  191. package/dist/observe/spans.d.ts.map +1 -1
  192. package/dist/observe/spans.js +20 -5
  193. package/dist/observe/spans.js.map +1 -1
  194. package/dist/schemes/EffectPolicy.d.ts +3 -0
  195. package/dist/schemes/EffectPolicy.d.ts.map +1 -1
  196. package/dist/schemes/EffectPolicy.js +38 -6
  197. package/dist/schemes/EffectPolicy.js.map +1 -1
  198. package/dist/schemes/Exec.d.ts.map +1 -1
  199. package/dist/schemes/Exec.js +82 -18
  200. package/dist/schemes/Exec.js.map +1 -1
  201. package/dist/schemes/Log.d.ts +6 -2
  202. package/dist/schemes/Log.d.ts.map +1 -1
  203. package/dist/schemes/Log.js +73 -22
  204. package/dist/schemes/Log.js.map +1 -1
  205. package/dist/schemes/Log.sql +25 -7
  206. package/dist/schemes/Prompt.d.ts.map +1 -1
  207. package/dist/schemes/Prompt.js +1 -2
  208. package/dist/schemes/Prompt.js.map +1 -1
  209. package/dist/schemes/QuestionTool.d.ts +10 -0
  210. package/dist/schemes/QuestionTool.d.ts.map +1 -0
  211. package/dist/schemes/QuestionTool.js +81 -0
  212. package/dist/schemes/QuestionTool.js.map +1 -0
  213. package/dist/schemes/Skill.d.ts.map +1 -1
  214. package/dist/schemes/Skill.js +4 -1
  215. package/dist/schemes/Skill.js.map +1 -1
  216. package/dist/schemes/Worker.d.ts.map +1 -1
  217. package/dist/schemes/Worker.js +20 -3
  218. package/dist/schemes/Worker.js.map +1 -1
  219. package/dist/schemes/_entry-crud.sql +19 -1
  220. package/dist/schemes/_entry-find.d.ts +5 -3
  221. package/dist/schemes/_entry-find.d.ts.map +1 -1
  222. package/dist/schemes/_entry-find.js +102 -22
  223. package/dist/schemes/_entry-find.js.map +1 -1
  224. package/dist/schemes/_entry-find.sql +8 -6
  225. package/dist/schemes/_entry-graph.sql +2 -6
  226. package/dist/schemes/_entry-manifest.d.ts.map +1 -1
  227. package/dist/schemes/_entry-manifest.js +12 -1
  228. package/dist/schemes/_entry-manifest.js.map +1 -1
  229. package/dist/schemes/_search-index.d.ts.map +1 -1
  230. package/dist/schemes/_search-index.js +37 -25
  231. package/dist/schemes/_search-index.js.map +1 -1
  232. package/dist/server/Daemon.d.ts +69 -12
  233. package/dist/server/Daemon.d.ts.map +1 -1
  234. package/dist/server/Daemon.js +737 -133
  235. package/dist/server/Daemon.js.map +1 -1
  236. package/dist/server/DaemonModule.d.ts +31 -3
  237. package/dist/server/DaemonModule.d.ts.map +1 -1
  238. package/dist/server/DrainSupervisor.d.ts +12 -9
  239. package/dist/server/DrainSupervisor.d.ts.map +1 -1
  240. package/dist/server/DrainSupervisor.js +59 -10
  241. package/dist/server/DrainSupervisor.js.map +1 -1
  242. package/dist/server/client-input.d.ts +7 -2
  243. package/dist/server/client-input.d.ts.map +1 -1
  244. package/dist/server/client-input.js +65 -34
  245. package/dist/server/client-input.js.map +1 -1
  246. package/dist/server/dispatch-as-plurnk.d.ts.map +1 -1
  247. package/dist/server/dispatch-as-plurnk.js +28 -6
  248. package/dist/server/dispatch-as-plurnk.js.map +1 -1
  249. package/dist/server/drain.sql +26 -11
  250. package/dist/server/envelope.d.ts +1 -1
  251. package/dist/server/envelope.d.ts.map +1 -1
  252. package/dist/server/envelope.js +4 -4
  253. package/dist/server/envelope.js.map +1 -1
  254. package/dist/server/envelope.sql +25 -1
  255. package/dist/server/lifecycle-recovery.sql +17 -1
  256. package/dist/server/logEntry.d.ts +2 -1
  257. package/dist/server/logEntry.d.ts.map +1 -1
  258. package/dist/server/logEntry.js +3 -2
  259. package/dist/server/logEntry.js.map +1 -1
  260. package/dist/server/logEntry.sql +7 -0
  261. package/dist/server/loop-model.d.ts +2 -2
  262. package/dist/server/loop-model.d.ts.map +1 -1
  263. package/dist/server/loop-model.js +23 -33
  264. package/dist/server/loop-model.js.map +1 -1
  265. package/dist/server/loopDocs.d.ts +1 -0
  266. package/dist/server/loopDocs.d.ts.map +1 -1
  267. package/dist/server/loopDocs.js +94 -14
  268. package/dist/server/loopDocs.js.map +1 -1
  269. package/dist/server/loopDocs.sql +12 -0
  270. package/dist/server/model-catalog.d.ts +3 -0
  271. package/dist/server/model-catalog.d.ts.map +1 -0
  272. package/dist/server/model-catalog.js +83 -0
  273. package/dist/server/model-catalog.js.map +1 -0
  274. package/dist/server/model-route.d.ts +7 -0
  275. package/dist/server/model-route.d.ts.map +1 -0
  276. package/dist/server/model-route.js +47 -0
  277. package/dist/server/model-route.js.map +1 -0
  278. package/dist/server/module-discovery.d.ts +13 -0
  279. package/dist/server/module-discovery.d.ts.map +1 -0
  280. package/dist/server/module-discovery.js +78 -0
  281. package/dist/server/module-discovery.js.map +1 -0
  282. package/dist/server/skillDocs.d.ts +9 -0
  283. package/dist/server/skillDocs.d.ts.map +1 -0
  284. package/dist/server/skillDocs.js +212 -0
  285. package/dist/server/skillDocs.js.map +1 -0
  286. package/dist/server/skillDocs.sql +14 -0
  287. package/dist/server/workspace-capabilities.sql +19 -0
  288. package/dist/service.d.ts.map +1 -1
  289. package/dist/service.js +182 -123
  290. package/dist/service.js.map +1 -1
  291. package/migrations/001_schema.sql +455 -65
  292. package/package.json +38 -37
  293. package/dist/core/ExecutableTools.d.ts +0 -10
  294. package/dist/core/ExecutableTools.d.ts.map +0 -1
  295. package/dist/core/ExecutableTools.js +0 -41
  296. package/dist/core/ExecutableTools.js.map +0 -1
  297. package/dist/core/JournalTurn.d.ts +0 -9
  298. package/dist/core/JournalTurn.d.ts.map +0 -1
  299. package/dist/core/JournalTurn.js +0 -14
  300. package/dist/core/JournalTurn.js.map +0 -1
  301. package/dist/core/JournalTurn.sql +0 -10
  302. package/dist/core/git-iso.d.ts +0 -13
  303. package/dist/core/git-iso.d.ts.map +0 -1
  304. package/dist/core/git-iso.js +0 -267
  305. package/dist/core/git-iso.js.map +0 -1
  306. package/dist/core/search-gate.d.ts +0 -17
  307. package/dist/core/search-gate.d.ts.map +0 -1
  308. package/dist/core/search-gate.js +0 -77
  309. package/dist/core/search-gate.js.map +0 -1
  310. package/dist/digest/Digest.sql +0 -1
@@ -19,6 +19,32 @@ 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
31
+ ) STRICT;
32
+
33
+ -- model_routes — the immutable resolved model route ({§worker-model-selection}). One row per
34
+ -- complete resolved tuple; append-only. Alias is provenance plus a tuning scope,
35
+ -- not route identity, and is absent on a direct provider/model selection.
36
+ CREATE TABLE IF NOT EXISTS model_routes (
37
+ id INTEGER NOT NULL PRIMARY KEY,
38
+ alias TEXT CHECK (alias IS NULL OR length(alias) > 0),
39
+ provider TEXT NOT NULL CHECK (length(provider) > 0),
40
+ model TEXT NOT NULL CHECK (length(model) > 0),
41
+ base_url TEXT CHECK (base_url IS NULL OR length(base_url) > 0),
42
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
43
+ ) STRICT;
44
+
45
+ CREATE UNIQUE INDEX IF NOT EXISTS model_routes_identity
46
+ ON model_routes (coalesce(alias, ''), provider, model, coalesce(base_url, ''));
47
+
22
48
  -- workers
23
49
  CREATE TABLE IF NOT EXISTS workers (
24
50
  id INTEGER NOT NULL PRIMARY KEY,
@@ -30,16 +56,31 @@ CREATE TABLE IF NOT EXISTS workers (
30
56
  -- independent databases reuse local integer ids.
31
57
  provider_identity TEXT NOT NULL DEFAULT (lower(hex(randomblob(16))))
32
58
  CHECK (length(provider_identity) = 32 AND provider_identity NOT GLOB '*[^0-9a-f]*'),
59
+ -- {§worker-model-selection}: the worker's durable resolved model, NULL for non-model
60
+ -- workers or a deliberately modelless unresolved worker. spawn_model_route_id is the
61
+ -- persistent spawn override; NULL means "use my model."
62
+ model_route_id INTEGER REFERENCES model_routes(id),
63
+ spawn_model_route_id INTEGER REFERENCES model_routes(id),
64
+ -- {§worker-reasoning-policy}: nullable only while the worker has no model;
65
+ -- once selected, model and reasoning policy form one durable generation policy.
66
+ reasoning_policy TEXT CHECK (reasoning_policy IS NULL OR reasoning_policy IN ('off', 'adaptive', 'low', 'medium', 'high')),
33
67
  -- workers fork via parent_worker_id; workspaces carry no parent — {§machine-processes-no-fork-workspace}
34
68
  parent_worker_id INTEGER CHECK (parent_worker_id IS NULL OR parent_worker_id != id),
35
- origin TEXT NOT NULL DEFAULT 'client' CHECK (origin IN ('model', 'client', 'plurnk')),
69
+ origin TEXT NOT NULL DEFAULT 'client' CHECK (origin IN ('model', 'client', '_plurnk')),
36
70
  -- {§methods-model-worker}: durable identity for the workspace's stable
37
71
  -- default conversation; unrelated to its human-facing, reclaimable name.
38
72
  default_conversation INTEGER NOT NULL DEFAULT 0 CHECK (default_conversation IN (0, 1)),
73
+ -- {§worker-settings}: the worker's own behavioral rules inside the workspace's
74
+ -- world — the workspace is how things are; each worker carries the rules its
75
+ -- loops obey. Client-declared at worker creation, mutable between loops,
76
+ -- validated at the client-input boundary (closed known-key set; unknown keys
77
+ -- never persist).
78
+ settings TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(settings)),
39
79
  -- {§env-delta-log-pull}: monotonic observation progress, not a private world snapshot.
40
80
  -- NULL means the worker has not established its first-turn baseline yet.
41
81
  ambient_event_cursor INTEGER CHECK (ambient_event_cursor IS NULL OR ambient_event_cursor >= 0),
42
82
  CHECK (default_conversation = 0 OR (origin = 'model' AND parent_worker_id IS NULL)),
83
+ CHECK ((model_route_id IS NULL) = (reasoning_policy IS NULL)),
43
84
  FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE,
44
85
  FOREIGN KEY (parent_worker_id) REFERENCES workers(id) ON DELETE CASCADE
45
86
  ) STRICT;
@@ -118,8 +159,11 @@ CREATE TABLE IF NOT EXISTS loops (
118
159
  status INTEGER NOT NULL DEFAULT 102 CHECK (status IN (100, 102, 200, 202, 413, 429, 499, 500, 504, 508)),
119
160
  prompt TEXT NOT NULL,
120
161
  flags TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(flags)),
121
- provider_spec TEXT NOT NULL DEFAULT 'null' CHECK (json_valid(provider_spec)),
122
- child_provider_spec TEXT NOT NULL DEFAULT 'null' CHECK (json_valid(child_provider_spec)),
162
+ -- {§worker-model-selection}: immutable loop snapshots of the resolved model route and the
163
+ -- effective spawn route (was provider_spec/child_provider_spec JSON).
164
+ model_route_id INTEGER REFERENCES model_routes(id),
165
+ spawn_model_route_id INTEGER REFERENCES model_routes(id),
166
+ reasoning_policy TEXT CHECK (reasoning_policy IS NULL OR reasoning_policy IN ('off', 'adaptive', 'low', 'medium', 'high')),
123
167
  max_turns INTEGER NOT NULL DEFAULT 50 CHECK (max_turns >= -1),
124
168
  -- {§methods-loop-run-open-paths}: the initial prompt frame's selected paths,
125
169
  -- held here until turn 1 materializes that frame (string[] JSON).
@@ -134,6 +178,9 @@ CREATE TABLE IF NOT EXISTS loops (
134
178
  -- {§loop-terminal-authorship}: 'cancel' names an external loop.cancel;
135
179
  -- NULL covers model terminals and engine verdicts whose result carries the story.
136
180
  terminated_by TEXT CHECK (terminated_by IS NULL OR terminated_by = 'cancel'),
181
+ CONSTRAINT loops_generation_policy_contract CHECK (
182
+ (model_route_id IS NULL) = (reasoning_policy IS NULL)
183
+ ),
137
184
  CONSTRAINT loops_terminal_result_contract CHECK (
138
185
  CASE
139
186
  WHEN status IN (100, 102, 202) THEN terminal_result IS NULL
@@ -180,7 +227,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS loops_worker_id_sequence ON loops (worker_id,
180
227
  CREATE UNIQUE INDEX IF NOT EXISTS loops_orphan_source_loop_id ON loops (orphan_source_loop_id);
181
228
  -- {§worker-scheme}: a loop crossing into a terminal status stamps terminated_at, so sibling
182
229
  -- workers pull the termination as a folded ambient delta — caught uniformly across every
183
- -- death-path (SEND, grinder, max-turns, strike, KILL). The stamp updates terminated_at,
230
+ -- death-path (SEND, overflow recovery, max-turns, strike, KILL). The stamp updates terminated_at,
184
231
  -- never status, so it cannot re-fire this trigger. Terminals: 200 done · 413 budget ·
185
232
  -- 429 turn-ceiling · 499 cancel · 500 fail · 504 wall-clock timeout · 508 runaway. (202 = parked/sleeping, NOT terminal.)
186
233
  CREATE TRIGGER IF NOT EXISTS loops_stamp_terminated_at
@@ -218,7 +265,15 @@ CREATE TABLE IF NOT EXISTS turns (
218
265
  loop_id INTEGER NOT NULL,
219
266
  sequence INTEGER NOT NULL CHECK (sequence >= 1),
220
267
  timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
268
+ -- {§turn-record}: producer and purpose define the turn. Packet/provider
269
+ -- evidence below is an optional inference specialization.
270
+ producer TEXT NOT NULL CHECK (producer IN ('model', 'client', '_plurnk', 'plugin')),
271
+ kind TEXT NOT NULL CHECK (kind IN ('inference', 'initialization', 'overflow', 'operation')),
221
272
  status INTEGER NOT NULL CHECK (status BETWEEN 100 AND 599),
273
+ -- NULL while the producer still owns this turn. Status 102 is both the
274
+ -- provisional running value and the exact completed continue disposition;
275
+ -- completed_at distinguishes those states without inventing another status.
276
+ completed_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
222
277
  -- Provider-derived curation calibration for this turn; NULL when input
223
278
  -- capacity is unknown. {§tokenomics-client-gauge}
224
279
  usage_curation_budget INTEGER CHECK (usage_curation_budget IS NULL OR usage_curation_budget >= 1),
@@ -253,16 +308,59 @@ CREATE TABLE IF NOT EXISTS turns (
253
308
  END
254
309
  ),
255
310
  finish_reason TEXT,
256
- model TEXT NOT NULL DEFAULT 'unknown' CHECK (length(model) >= 1),
257
- -- Opaque provider→client metadata plus engine rail keys; JSON-valid but
258
- -- otherwise unenforced. {§meta-passthrough}, {§rail-truth-engine-verdict}
259
- meta TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(meta)),
311
+ model TEXT CHECK (model IS NULL OR length(model) >= 1),
312
+ -- Opaque provider→client metadata plus engine rail keys; absent outside
313
+ -- recorded inference evidence. {§meta-passthrough}, {§rail-truth-engine-verdict}
314
+ meta TEXT CHECK (meta IS NULL OR json_valid(meta)),
315
+ CHECK (completed_at IS NOT NULL OR status = 102),
316
+ CHECK ((producer = 'model') = (kind = 'inference')),
317
+ CHECK (kind NOT IN ('initialization', 'overflow') OR producer = '_plurnk'),
318
+ CHECK (
319
+ kind = 'inference'
320
+ OR (
321
+ usage_curation_budget IS NULL
322
+ AND packet IS NULL
323
+ AND finish_reason IS NULL
324
+ AND model IS NULL
325
+ AND meta IS NULL
326
+ )
327
+ ),
260
328
  FOREIGN KEY (loop_id) REFERENCES loops(id) ON DELETE CASCADE
261
329
  ) STRICT;
262
330
 
263
331
  CREATE UNIQUE INDEX IF NOT EXISTS turns_loop_id_sequence ON turns (loop_id, sequence);
264
332
  CREATE INDEX IF NOT EXISTS turns_timestamp ON turns (timestamp);
265
333
 
334
+ -- Producer and purpose are immutable except for the single pre-inference
335
+ -- diversion into overflow recovery. The transition is allowed only before any
336
+ -- model or non-kernel evidence exists.
337
+ CREATE TRIGGER IF NOT EXISTS turns_identity_forward_only
338
+ BEFORE UPDATE OF producer, kind ON turns
339
+ WHEN NOT (
340
+ OLD.producer = NEW.producer
341
+ AND OLD.kind = NEW.kind
342
+ )
343
+ AND NOT (
344
+ OLD.producer = 'model'
345
+ AND OLD.kind = 'inference'
346
+ AND NEW.producer = '_plurnk'
347
+ AND NEW.kind = 'overflow'
348
+ AND OLD.completed_at IS NULL
349
+ AND OLD.packet IS NULL
350
+ AND OLD.usage_curation_budget IS NULL
351
+ AND OLD.finish_reason IS NULL
352
+ AND OLD.model IS NULL
353
+ AND OLD.meta IS NULL
354
+ AND NOT EXISTS (SELECT 1 FROM model_calls WHERE turn_id = OLD.id)
355
+ AND NOT EXISTS (
356
+ SELECT 1 FROM log_entries
357
+ WHERE turn_id = OLD.id AND origin != '_plurnk'
358
+ )
359
+ )
360
+ BEGIN
361
+ SELECT RAISE(ABORT, 'turn producer and kind are immutable outside pre-inference overflow diversion');
362
+ END;
363
+
266
364
  -- One logical provider.generate call. Emission attempts and BARE inferences
267
365
  -- share response/failure evidence and cardinal physical request accounting;
268
366
  -- operation-specific semantics live in their specializing relations.
@@ -310,6 +408,17 @@ CREATE TABLE IF NOT EXISTS model_calls (
310
408
 
311
409
  CREATE INDEX IF NOT EXISTS model_calls_turn_id ON model_calls (turn_id, sequence);
312
410
 
411
+ CREATE TRIGGER IF NOT EXISTS model_calls_inference_turn_only
412
+ BEFORE INSERT ON model_calls
413
+ WHEN COALESCE((
414
+ SELECT producer = 'model' AND kind = 'inference'
415
+ FROM turns
416
+ WHERE id = NEW.turn_id
417
+ ), 0) != 1
418
+ BEGIN
419
+ SELECT RAISE(ABORT, 'model call requires a model inference turn');
420
+ END;
421
+
313
422
  CREATE TRIGGER IF NOT EXISTS model_calls_request_identity_immutable
314
423
  BEFORE UPDATE OF turn_id, sequence, kind, attributions, timestamp ON model_calls
315
424
  BEGIN
@@ -521,9 +630,9 @@ BEGIN
521
630
  END;
522
631
 
523
632
  -- derivations
524
- -- Content-addressed deep projections. Entries point at a COMPLETE artifact by
525
- -- deep_hash; graph, FTS, and vectors are stored once regardless of how many
526
- -- workspace/worktree paths carry identical content under the same reader/config.
633
+ -- Content-addressed deep projections. Entry channels and log projections point
634
+ -- at a COMPLETE artifact by deep_hash; graph, FTS, and vectors are stored once
635
+ -- regardless of how many addresses carry identical content under the same reader/config.
527
636
  -- A building row is unattached and safely replaceable after interruption.
528
637
  CREATE TABLE IF NOT EXISTS derivations (
529
638
  id INTEGER NOT NULL PRIMARY KEY,
@@ -532,6 +641,14 @@ CREATE TABLE IF NOT EXISTS derivations (
532
641
  disposition TEXT CHECK (disposition IN ('vector', 'lexical', 'excluded', 'nonsemantic', 'failed')),
533
642
  reason TEXT,
534
643
  parse_issues INTEGER CHECK (parse_issues IS NULL OR parse_issues > 0),
644
+ summary TEXT CHECK (
645
+ summary IS NULL OR (
646
+ length(summary) > 0
647
+ AND summary = trim(summary)
648
+ AND instr(summary, char(10)) = 0
649
+ AND instr(summary, char(13)) = 0
650
+ )
651
+ ),
535
652
  CHECK ((state = 'building' AND disposition IS NULL) OR (state = 'complete' AND disposition IS NOT NULL))
536
653
  ) STRICT;
537
654
 
@@ -561,11 +678,6 @@ CREATE TABLE IF NOT EXISTS entries (
561
678
  -- == members; 'client'/'constraint' (model-created, add-glob) are not git's to reclaim.
562
679
  -- NULL = not a file member (other schemes don't carry origin).
563
680
  membership_origin TEXT CHECK (membership_origin IS NULL OR membership_origin IN ('git', 'client', 'constraint')),
564
- -- {§membership-change-gated-sync}: hash of the body content at the last
565
- -- deep-channel derivation. The manifest-add pass re-derives symbols/refs (and
566
- -- embeddings, later) ONLY when this differs from the current body hash — an
567
- -- unchanged entry is skipped, never re-metadatafied every turn.
568
- deep_hash TEXT,
569
681
  -- SPEC {§membership-change-gated-sync} — the per-member sync stat-detect:
570
682
  -- "<mtimeMs>:<size>" of the disk file at its last materialization, or `absent`
571
683
  -- after an observed deletion. The pre-turn
@@ -573,13 +685,13 @@ CREATE TABLE IF NOT EXISTS entries (
573
685
  -- signature changed; an unchanged member is a no-op. NULL = never synced.
574
686
  synced_sig TEXT,
575
687
  -- User Note 5 — manifest cache-friendliness. Last-modified stamp, bumped on every
576
- -- channel write by entries_touch_on_channel_write; engine_list_workspace_entries orders
577
- -- the catalog by it ASC so dormant entries hold the stable prompt-cache prefix.
688
+ -- addressable representation change; engine_list_workspace_entries orders the catalog
689
+ -- by it ASC so dormant entries hold the stable prompt-cache prefix. Private derivation
690
+ -- attachment does not make an entry recently touched.
578
691
  updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
579
692
  CHECK (workspace_id IS NOT NULL),
580
693
  FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE,
581
- FOREIGN KEY (owner_id) REFERENCES workers(id) ON DELETE CASCADE,
582
- FOREIGN KEY (deep_hash) REFERENCES derivations(deep_hash)
694
+ FOREIGN KEY (owner_id) REFERENCES workers(id) ON DELETE CASCADE
583
695
  ) STRICT;
584
696
 
585
697
  -- {§entry-owner} / {§stream-owner-scoped} — owner is the sole visibility and identity axis.
@@ -599,6 +711,9 @@ CREATE TABLE IF NOT EXISTS entry_channels (
599
711
  -- Content identity: sha256 of content, stamped at static writes; streamed
600
712
  -- appends leave it NULL. Curation weight remains model-independent.
601
713
  content_hash TEXT,
714
+ -- Search derivation for this exact addressable channel representation.
715
+ -- Content or mimetype changes invalidate it at the owning channel row.
716
+ deep_hash TEXT,
602
717
  state TEXT NOT NULL DEFAULT 'static' CHECK (state IN ('static', 'active', 'closed', 'errored')),
603
718
  -- Exact terminal producer evidence for this representation channel. NULL is
604
719
  -- the ordinary implicit {status:200}; selection/projection metadata belongs
@@ -631,12 +746,26 @@ CREATE TABLE IF NOT EXISTS entry_channels (
631
746
  END
632
747
  ),
633
748
  PRIMARY KEY (entry_id, name),
634
- FOREIGN KEY (entry_id) REFERENCES entries(id) ON DELETE CASCADE
749
+ FOREIGN KEY (entry_id) REFERENCES entries(id) ON DELETE CASCADE,
750
+ FOREIGN KEY (deep_hash) REFERENCES derivations(deep_hash)
635
751
  ) STRICT, WITHOUT ROWID;
636
752
 
637
- -- User Note 5 — bump the entry's updated_at on any channel write so the catalog
638
- -- (ordered by updated_at ASC) keeps recently-touched entries at the tail and holds
639
- -- the prompt-cache prefix stable across turns.
753
+ -- A changed channel representation cannot retain search evidence derived from
754
+ -- its predecessor. This trigger is the one invalidation owner for every write
755
+ -- path, including model EDIT, plugin channel capabilities, and streams.
756
+ CREATE TRIGGER IF NOT EXISTS entry_channels_invalidate_derivation
757
+ AFTER UPDATE OF content, mimetype ON entry_channels
758
+ WHEN OLD.content IS NOT NEW.content OR OLD.mimetype IS NOT NEW.mimetype
759
+ BEGIN
760
+ UPDATE entry_channels
761
+ SET deep_hash = NULL
762
+ WHERE entry_id = NEW.entry_id AND name = NEW.name AND deep_hash IS NOT NULL;
763
+ END;
764
+
765
+ -- User Note 5 — bump the entry's updated_at on addressable representation or
766
+ -- lifecycle writes so the catalog (ordered by updated_at ASC) keeps recently-
767
+ -- touched entries at the tail and holds the prompt-cache prefix stable across
768
+ -- turns. Content hashes and search attachments are private metadata, not touches.
640
769
  CREATE TRIGGER IF NOT EXISTS entries_touch_on_channel_write
641
770
  AFTER INSERT ON entry_channels
642
771
  BEGIN
@@ -644,7 +773,7 @@ BEGIN
644
773
  END;
645
774
 
646
775
  CREATE TRIGGER IF NOT EXISTS entries_touch_on_channel_update
647
- AFTER UPDATE ON entry_channels
776
+ AFTER UPDATE OF content, mimetype, weight, state, producer_result ON entry_channels
648
777
  BEGIN
649
778
  UPDATE entries SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = NEW.entry_id;
650
779
  END;
@@ -720,7 +849,8 @@ CREATE TABLE IF NOT EXISTS derivation_embeddings (
720
849
  -- status⊥state: status is the HTTP outcome, state is where in the
721
850
  -- lifecycle the entry sits. Most rows write 'resolved' directly;
722
851
  -- proposing schemes transition 'proposed' → resolved/failed/cancelled.
723
- -- expanded: per-row visibility for OPEN/FOLD via the log:/// scheme.
852
+ -- folded: canonical hidden log-body line intervals for OPEN/FOLD. [] is
853
+ -- wholly open and [[1,-1]] is wholly folded.
724
854
  CREATE TABLE IF NOT EXISTS log_entries (
725
855
  id INTEGER NOT NULL PRIMARY KEY,
726
856
  version INTEGER NOT NULL DEFAULT 0 CHECK (version >= 0),
@@ -730,7 +860,7 @@ CREATE TABLE IF NOT EXISTS log_entries (
730
860
  turn_id INTEGER NOT NULL,
731
861
  sequence INTEGER NOT NULL CHECK (sequence >= 1),
732
862
  at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
733
- origin TEXT NOT NULL CHECK (origin IN ('model', 'client', 'plurnk', 'plugin')),
863
+ origin TEXT NOT NULL CHECK (origin IN ('model', 'client', '_plurnk', 'plugin')),
734
864
  -- {§env-delta-attribution}: a worker:// name or stable subsystem token ('file');
735
865
  -- NULL = the owning worker itself, rendered without causal attribution.
736
866
  source TEXT,
@@ -739,21 +869,22 @@ CREATE TABLE IF NOT EXISTS log_entries (
739
869
  ambient_event_id INTEGER REFERENCES ambient_events(id),
740
870
  -- Search derivation attached to this durable log result, when available.
741
871
  deep_hash TEXT REFERENCES derivations(deep_hash),
742
- -- Exact logical provider call represented by a BARE result or model-emission
743
- -- mirror. Other operation and ambient rows carry no model-call identity.
872
+ -- Exact logical provider call represented by a BARE result, admitted
873
+ -- model turnOps, or rejected emissionAttempt. Other operation and ambient
874
+ -- rows carry no model-call identity.
744
875
  model_call_id INTEGER REFERENCES model_calls(id),
745
876
 
746
877
  -- 'error' is an ACTIONLESS row ({§operation-results} — errors are log items): a parse failure that
747
878
  -- produced no op still records a log entry (op='error', status_rx≥400, no target) so the model
748
879
  -- can fold/kill/recall its own mistakes like any other log row — one budget surface, the log.
749
- -- Actionless artifacts carry NULL here: kernel-authored worker initialization
750
- -- ({§worker-initialization-entry}) or a model emission ({§model-entry}).
880
+ -- Exact producer source and rejected-attempt artifacts carry NULL here
881
+ -- ({§turn-ops-entry}, {§rejected-emission-entry}).
751
882
  -- No op enum here: the grammar op set is grammar's contract (PlurnkOp), and this column is written
752
883
  -- only by the PlurnkOp-typed engine (grammar ops), service row selectors, or NULL for no op.
753
884
  -- A SQL enum would be a hand-copy of grammar's op list that silently goes stale on every new verb
754
885
  -- (it did — FORK/WORK). Validity lives at the parse + type layer, not duplicated in DDL.
755
886
  op TEXT,
756
- suffix TEXT NOT NULL DEFAULT '',
887
+ delimiter TEXT NOT NULL DEFAULT '',
757
888
  signal TEXT CHECK (signal IS NULL OR json_valid(signal)),
758
889
 
759
890
  scheme TEXT CHECK (scheme IS NULL OR length(scheme) > 0),
@@ -784,11 +915,16 @@ CREATE TABLE IF NOT EXISTS log_entries (
784
915
  outcome TEXT,
785
916
  attrs TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(attrs)),
786
917
 
787
- expanded INTEGER NOT NULL DEFAULT 1 CHECK (expanded IN (0, 1)),
918
+ folded TEXT NOT NULL DEFAULT '[]'
919
+ CHECK (json_valid(folded) AND json_type(folded) = 'array'),
788
920
 
789
- CHECK ((op IS NULL) = COALESCE(json_extract(attrs, '$.kind') IN ('initialization', 'model_emission'), 0)),
790
- CHECK (json_extract(attrs, '$.kind') != 'initialization' OR origin = 'plurnk'),
791
- CHECK (json_extract(attrs, '$.kind') != 'model_emission' OR origin = 'model'),
921
+ CHECK (
922
+ (op IS NULL) = COALESCE(
923
+ json_extract(attrs, '$.kind') IN ('turnOps', 'emissionAttempt'),
924
+ 0
925
+ )
926
+ ),
927
+ CHECK (json_extract(attrs, '$.kind') != 'emissionAttempt' OR origin = 'model'),
792
928
 
793
929
  FOREIGN KEY (worker_id) REFERENCES workers(id) ON DELETE CASCADE,
794
930
  FOREIGN KEY (loop_id) REFERENCES loops(id) ON DELETE CASCADE,
@@ -806,6 +942,108 @@ CREATE UNIQUE INDEX IF NOT EXISTS log_entries_worker_ambient_event
806
942
  ON log_entries (worker_id, ambient_event_id)
807
943
  WHERE ambient_event_id IS NOT NULL;
808
944
 
945
+ -- A log row belongs to one exact worker/loop/turn chain. Its writer is either
946
+ -- that turn's producer or `_plurnk` observing the turn. Primitive absence,
947
+ -- enum, and foreign-key failures remain owned by their column constraints.
948
+ CREATE TRIGGER IF NOT EXISTS log_entries_turn_ownership
949
+ BEFORE INSERT ON log_entries
950
+ WHEN NEW.worker_id IS NOT NULL
951
+ AND NEW.loop_id IS NOT NULL
952
+ AND NEW.turn_id IS NOT NULL
953
+ AND NEW.origin IN ('model', 'client', '_plurnk', 'plugin')
954
+ AND EXISTS (SELECT 1 FROM workers WHERE id = NEW.worker_id)
955
+ AND EXISTS (SELECT 1 FROM loops WHERE id = NEW.loop_id)
956
+ AND EXISTS (SELECT 1 FROM turns WHERE id = NEW.turn_id)
957
+ AND NOT EXISTS (
958
+ SELECT 1
959
+ FROM turns
960
+ JOIN loops ON loops.id = turns.loop_id
961
+ WHERE turns.id = NEW.turn_id
962
+ AND loops.id = NEW.loop_id
963
+ AND loops.worker_id = NEW.worker_id
964
+ AND (NEW.origin = turns.producer OR NEW.origin = '_plurnk')
965
+ )
966
+ BEGIN
967
+ SELECT RAISE(ABORT, 'log entry must match its turn ownership and producer');
968
+ END;
969
+
970
+ -- {§turn-ops-entry} — exact admitted source is authored by the turn producer.
971
+ -- `_plurnk` may observe another producer's turn through ordinary operation
972
+ -- rows, but it cannot impersonate that producer's source program.
973
+ CREATE TRIGGER IF NOT EXISTS log_entries_turn_ops_producer
974
+ BEFORE INSERT ON log_entries
975
+ WHEN NEW.op IS NULL
976
+ AND json_extract(NEW.attrs, '$.kind') = 'turnOps'
977
+ AND EXISTS (SELECT 1 FROM turns WHERE id = NEW.turn_id)
978
+ AND NOT EXISTS (
979
+ SELECT 1
980
+ FROM turns
981
+ WHERE turns.id = NEW.turn_id
982
+ AND turns.producer = NEW.origin
983
+ )
984
+ BEGIN
985
+ SELECT RAISE(ABORT, 'actionless log entry does not match its turn producer');
986
+ END;
987
+
988
+ -- A folded interval is an inclusive [start,end] pair. Ranges are positive,
989
+ -- sorted, disjoint, and non-adjacent; -1 is the final open-ended endpoint.
990
+ -- Canonical intervals make visibility equality and curation effects exact.
991
+ CREATE TRIGGER IF NOT EXISTS log_entries_folded_valid_insert
992
+ BEFORE INSERT ON log_entries
993
+ WHEN EXISTS (
994
+ SELECT 1
995
+ FROM json_each(NEW.folded) range
996
+ WHERE range.type != 'array'
997
+ OR json_array_length(range.value) != 2
998
+ OR COALESCE(json_type(range.value, '$[0]'), '') != 'integer'
999
+ OR COALESCE(json_type(range.value, '$[1]'), '') != 'integer'
1000
+ OR json_extract(range.value, '$[0]') < 1
1001
+ OR (
1002
+ json_extract(range.value, '$[1]') != -1
1003
+ AND json_extract(range.value, '$[1]') < json_extract(range.value, '$[0]')
1004
+ )
1005
+ OR EXISTS (
1006
+ SELECT 1
1007
+ FROM json_each(NEW.folded) previous
1008
+ WHERE previous.key = range.key - 1
1009
+ AND (
1010
+ json_extract(previous.value, '$[1]') = -1
1011
+ OR json_extract(range.value, '$[0]') <= json_extract(previous.value, '$[1]') + 1
1012
+ )
1013
+ )
1014
+ )
1015
+ BEGIN
1016
+ SELECT RAISE(ABORT, 'log entry folded ranges are invalid');
1017
+ END;
1018
+
1019
+ CREATE TRIGGER IF NOT EXISTS log_entries_folded_valid_update
1020
+ BEFORE UPDATE OF folded ON log_entries
1021
+ WHEN EXISTS (
1022
+ SELECT 1
1023
+ FROM json_each(NEW.folded) range
1024
+ WHERE range.type != 'array'
1025
+ OR json_array_length(range.value) != 2
1026
+ OR COALESCE(json_type(range.value, '$[0]'), '') != 'integer'
1027
+ OR COALESCE(json_type(range.value, '$[1]'), '') != 'integer'
1028
+ OR json_extract(range.value, '$[0]') < 1
1029
+ OR (
1030
+ json_extract(range.value, '$[1]') != -1
1031
+ AND json_extract(range.value, '$[1]') < json_extract(range.value, '$[0]')
1032
+ )
1033
+ OR EXISTS (
1034
+ SELECT 1
1035
+ FROM json_each(NEW.folded) previous
1036
+ WHERE previous.key = range.key - 1
1037
+ AND (
1038
+ json_extract(previous.value, '$[1]') = -1
1039
+ OR json_extract(range.value, '$[0]') <= json_extract(previous.value, '$[1]') + 1
1040
+ )
1041
+ )
1042
+ )
1043
+ BEGIN
1044
+ SELECT RAISE(ABORT, 'log entry folded ranges are invalid');
1045
+ END;
1046
+
809
1047
  CREATE TRIGGER IF NOT EXISTS log_entries_model_call_valid
810
1048
  BEFORE INSERT ON log_entries
811
1049
  WHEN NEW.model_call_id IS NOT NULL
@@ -820,7 +1058,7 @@ WHEN NEW.model_call_id IS NOT NULL
820
1058
  OR (
821
1059
  call.kind = 'emission'
822
1060
  AND NEW.op IS NULL
823
- AND json_extract(NEW.attrs, '$.kind') = 'model_emission'
1061
+ AND json_extract(NEW.attrs, '$.kind') IN ('turnOps', 'emissionAttempt')
824
1062
  )
825
1063
  )
826
1064
  )
@@ -924,14 +1162,61 @@ BEGIN
924
1162
  AND source_record_id = NEW.id;
925
1163
  END;
926
1164
 
1165
+ -- {§worker-initialization-entry} {§overflow-turn-script} — the turn identity
1166
+ -- classifies every row in a kernel-authored recovery/orientation turn. No
1167
+ -- caller can omit provenance from either its exact turnOps or its operation
1168
+ -- outcomes.
1169
+ CREATE TRIGGER IF NOT EXISTS log_entries_classify_plurnk_turn
1170
+ AFTER INSERT ON log_entries
1171
+ WHEN EXISTS (
1172
+ SELECT 1
1173
+ FROM turns
1174
+ WHERE turns.id = NEW.turn_id
1175
+ AND turns.producer = '_plurnk'
1176
+ AND turns.kind IN ('initialization', 'overflow')
1177
+ )
1178
+ BEGIN
1179
+ INSERT OR IGNORE INTO log_tags (log_entry_id, tag)
1180
+ VALUES (NEW.id, '_plurnk');
1181
+ INSERT OR IGNORE INTO log_tags (log_entry_id, tag)
1182
+ SELECT NEW.id, CASE turns.kind
1183
+ WHEN 'initialization' THEN 'init'
1184
+ ELSE 'overflow'
1185
+ END
1186
+ FROM turns
1187
+ WHERE turns.id = NEW.turn_id;
1188
+ END;
1189
+
1190
+ -- Overflow admission reclassifies a would-be inference turn only after its
1191
+ -- pre-model rows exist. Apply the same turn-owned classification to those
1192
+ -- rows; later rows are covered by log_entries_classify_plurnk_turn above.
1193
+ CREATE TRIGGER IF NOT EXISTS turns_classify_existing_plurnk_rows
1194
+ AFTER UPDATE OF producer, kind ON turns
1195
+ WHEN NEW.producer = '_plurnk'
1196
+ AND NEW.kind IN ('initialization', 'overflow')
1197
+ BEGIN
1198
+ INSERT OR IGNORE INTO log_tags (log_entry_id, tag)
1199
+ SELECT id, '_plurnk' FROM log_entries WHERE turn_id = NEW.id;
1200
+ INSERT OR IGNORE INTO log_tags (log_entry_id, tag)
1201
+ SELECT id, CASE NEW.kind
1202
+ WHEN 'initialization' THEN 'init'
1203
+ ELSE 'overflow'
1204
+ END
1205
+ FROM log_entries
1206
+ WHERE turn_id = NEW.id;
1207
+ END;
1208
+
927
1209
  -- Successful OPEN/FOLD rows are durable curation events even though their
928
1210
  -- ordinary packet projection is suppressed ({§fold-open-meta-operations}).
929
- -- Preserve the exact selected set and each target's pre-event visibility so a
1211
+ -- Preserve the exact selected set and each target's before/after visibility so a
930
1212
  -- broad selector never collapses into the lossy fact `matched: N`.
931
1213
  CREATE TABLE IF NOT EXISTS log_curation_effects (
932
1214
  operation_log_entry_id INTEGER NOT NULL,
933
1215
  target_log_entry_id INTEGER NOT NULL,
934
- expanded_before INTEGER NOT NULL CHECK (expanded_before IN (0, 1)),
1216
+ folded_before TEXT NOT NULL
1217
+ CHECK (json_valid(folded_before) AND json_type(folded_before) = 'array'),
1218
+ folded_after TEXT NOT NULL
1219
+ CHECK (json_valid(folded_after) AND json_type(folded_after) = 'array'),
935
1220
  tags_added TEXT NOT NULL DEFAULT '[]'
936
1221
  CHECK (json_valid(tags_added) AND json_type(tags_added) = 'array'),
937
1222
  tags_removed TEXT NOT NULL DEFAULT '[]'
@@ -961,6 +1246,39 @@ BEGIN
961
1246
  AND operation.status_rx < 400
962
1247
  AND operation.worker_id = target.worker_id
963
1248
  )
1249
+ OR EXISTS (
1250
+ SELECT 1
1251
+ FROM (
1252
+ SELECT 'before' AS side, key, value, type FROM json_each(NEW.folded_before)
1253
+ UNION ALL
1254
+ SELECT 'after' AS side, key, value, type FROM json_each(NEW.folded_after)
1255
+ ) range
1256
+ WHERE range.type != 'array'
1257
+ OR json_array_length(range.value) != 2
1258
+ OR COALESCE(json_type(range.value, '$[0]'), '') != 'integer'
1259
+ OR COALESCE(json_type(range.value, '$[1]'), '') != 'integer'
1260
+ OR json_extract(range.value, '$[0]') < 1
1261
+ OR (
1262
+ json_extract(range.value, '$[1]') != -1
1263
+ AND json_extract(range.value, '$[1]') < json_extract(range.value, '$[0]')
1264
+ )
1265
+ OR EXISTS (
1266
+ SELECT 1
1267
+ FROM (
1268
+ SELECT key, value FROM json_each(
1269
+ CASE range.side
1270
+ WHEN 'before' THEN NEW.folded_before
1271
+ ELSE NEW.folded_after
1272
+ END
1273
+ )
1274
+ ) previous
1275
+ WHERE previous.key = range.key - 1
1276
+ AND (
1277
+ json_extract(previous.value, '$[1]') = -1
1278
+ OR json_extract(range.value, '$[0]') <= json_extract(previous.value, '$[1]') + 1
1279
+ )
1280
+ )
1281
+ )
964
1282
  OR EXISTS (
965
1283
  SELECT 1
966
1284
  FROM (
@@ -1016,7 +1334,7 @@ BEGIN
1016
1334
  END;
1017
1335
 
1018
1336
  -- One outer INSERT owns the whole landed curation event: exact selected rows,
1019
- -- their pre-event visibility and tag deltas, the requested visibility, and the
1337
+ -- their before/after visibility and tag deltas, and the
1020
1338
  -- resulting classifications. Trigger failure rolls the operation row and all
1021
1339
  -- effects back together. The private plan is erased before INSERT returns.
1022
1340
  CREATE TRIGGER IF NOT EXISTS log_entries_apply_curation
@@ -1026,26 +1344,70 @@ WHEN NEW.op IN ('OPEN', 'FOLD')
1026
1344
  AND json_type(NEW.attrs, '$.__plurnk_curation') = 'object'
1027
1345
  BEGIN
1028
1346
  SELECT CASE WHEN
1029
- COALESCE(json_type(NEW.attrs, '$.__plurnk_curation.ids'), '') != 'array'
1347
+ COALESCE(json_type(NEW.attrs, '$.__plurnk_curation.targets'), '') != 'array'
1030
1348
  OR COALESCE(json_type(NEW.attrs, '$.__plurnk_curation.add'), '') != 'array'
1031
1349
  OR COALESCE(json_type(NEW.attrs, '$.__plurnk_curation.remove'), '') != 'array'
1032
- OR COALESCE(json_type(NEW.attrs, '$.__plurnk_curation.expanded'), '') != 'integer'
1033
- OR json_extract(NEW.attrs, '$.__plurnk_curation.expanded') NOT IN (0, 1)
1034
- OR json_array_length(NEW.attrs, '$.__plurnk_curation.ids') = 0
1350
+ OR json_array_length(NEW.attrs, '$.__plurnk_curation.targets') = 0
1035
1351
  OR EXISTS (
1036
- SELECT 1 FROM json_each(NEW.attrs, '$.__plurnk_curation.ids')
1037
- WHERE type != 'integer' OR value <= 0
1352
+ SELECT 1
1353
+ FROM json_each(NEW.attrs, '$.__plurnk_curation.targets') selected
1354
+ WHERE selected.type != 'object'
1355
+ OR COALESCE(json_type(selected.value, '$.id'), '') != 'integer'
1356
+ OR json_extract(selected.value, '$.id') <= 0
1357
+ OR COALESCE(json_type(selected.value, '$.before'), '') != 'array'
1358
+ OR COALESCE(json_type(selected.value, '$.after'), '') != 'array'
1359
+ OR EXISTS (
1360
+ SELECT 1 FROM json_each(selected.value) field
1361
+ WHERE field.key NOT IN ('id', 'before', 'after')
1362
+ )
1363
+ OR EXISTS (
1364
+ SELECT 1
1365
+ FROM (
1366
+ SELECT 'before' AS side, range.key, range.value, range.type
1367
+ FROM json_each(json_extract(selected.value, '$.before')) range
1368
+ UNION ALL
1369
+ SELECT 'after' AS side, range.key, range.value, range.type
1370
+ FROM json_each(json_extract(selected.value, '$.after')) range
1371
+ ) range
1372
+ WHERE range.type != 'array'
1373
+ OR json_array_length(range.value) != 2
1374
+ OR COALESCE(json_type(range.value, '$[0]'), '') != 'integer'
1375
+ OR COALESCE(json_type(range.value, '$[1]'), '') != 'integer'
1376
+ OR json_extract(range.value, '$[0]') < 1
1377
+ OR (
1378
+ json_extract(range.value, '$[1]') != -1
1379
+ AND json_extract(range.value, '$[1]') < json_extract(range.value, '$[0]')
1380
+ )
1381
+ OR EXISTS (
1382
+ SELECT 1
1383
+ FROM json_each(
1384
+ CASE range.side
1385
+ WHEN 'before' THEN json_extract(selected.value, '$.before')
1386
+ ELSE json_extract(selected.value, '$.after')
1387
+ END
1388
+ ) previous
1389
+ WHERE previous.key = range.key - 1
1390
+ AND (
1391
+ json_extract(previous.value, '$[1]') = -1
1392
+ OR json_extract(range.value, '$[0]') <= json_extract(previous.value, '$[1]') + 1
1393
+ )
1394
+ )
1395
+ )
1038
1396
  )
1039
1397
  OR (
1040
- SELECT COUNT(*) FROM json_each(NEW.attrs, '$.__plurnk_curation.ids')
1398
+ SELECT COUNT(*) FROM json_each(NEW.attrs, '$.__plurnk_curation.targets')
1041
1399
  ) != (
1042
- SELECT COUNT(DISTINCT value) FROM json_each(NEW.attrs, '$.__plurnk_curation.ids')
1400
+ SELECT COUNT(DISTINCT json_extract(value, '$.id'))
1401
+ FROM json_each(NEW.attrs, '$.__plurnk_curation.targets')
1043
1402
  )
1044
1403
  OR EXISTS (
1045
1404
  SELECT 1
1046
- FROM json_each(NEW.attrs, '$.__plurnk_curation.ids') selected
1047
- LEFT JOIN log_entries target ON target.id = selected.value
1048
- WHERE target.id IS NULL OR target.worker_id != NEW.worker_id OR target.id = NEW.id
1405
+ FROM json_each(NEW.attrs, '$.__plurnk_curation.targets') selected
1406
+ LEFT JOIN log_entries target ON target.id = json_extract(selected.value, '$.id')
1407
+ WHERE target.id IS NULL
1408
+ OR target.worker_id != NEW.worker_id
1409
+ OR target.id = NEW.id
1410
+ OR json(target.folded) != json(json_extract(selected.value, '$.before'))
1049
1411
  )
1050
1412
  OR EXISTS (
1051
1413
  SELECT 1
@@ -1096,14 +1458,16 @@ BEGIN
1096
1458
  INSERT INTO log_curation_effects (
1097
1459
  operation_log_entry_id,
1098
1460
  target_log_entry_id,
1099
- expanded_before,
1461
+ folded_before,
1462
+ folded_after,
1100
1463
  tags_added,
1101
1464
  tags_removed
1102
1465
  )
1103
1466
  SELECT
1104
1467
  NEW.id,
1105
1468
  target.id,
1106
- target.expanded,
1469
+ json_extract(selected.value, '$.before'),
1470
+ json_extract(selected.value, '$.after'),
1107
1471
  COALESCE((
1108
1472
  SELECT json_group_array(ordered.tag)
1109
1473
  FROM (
@@ -1128,26 +1492,32 @@ BEGIN
1128
1492
  ORDER BY removal.value
1129
1493
  ) ordered
1130
1494
  ), '[]')
1131
- FROM json_each(NEW.attrs, '$.__plurnk_curation.ids') selected
1132
- JOIN log_entries target ON target.id = selected.value;
1495
+ FROM json_each(NEW.attrs, '$.__plurnk_curation.targets') selected
1496
+ JOIN log_entries target ON target.id = json_extract(selected.value, '$.id');
1133
1497
 
1134
1498
  UPDATE log_entries
1135
- SET expanded = json_extract(NEW.attrs, '$.__plurnk_curation.expanded')
1499
+ SET folded = (
1500
+ SELECT json_extract(selected.value, '$.after')
1501
+ FROM json_each(NEW.attrs, '$.__plurnk_curation.targets') selected
1502
+ WHERE json_extract(selected.value, '$.id') = log_entries.id
1503
+ )
1136
1504
  WHERE id IN (
1137
- SELECT value FROM json_each(NEW.attrs, '$.__plurnk_curation.ids')
1505
+ SELECT json_extract(value, '$.id')
1506
+ FROM json_each(NEW.attrs, '$.__plurnk_curation.targets')
1138
1507
  );
1139
1508
 
1140
1509
  DELETE FROM log_tags
1141
1510
  WHERE log_entry_id IN (
1142
- SELECT value FROM json_each(NEW.attrs, '$.__plurnk_curation.ids')
1511
+ SELECT json_extract(value, '$.id')
1512
+ FROM json_each(NEW.attrs, '$.__plurnk_curation.targets')
1143
1513
  )
1144
1514
  AND tag IN (
1145
1515
  SELECT value FROM json_each(NEW.attrs, '$.__plurnk_curation.remove')
1146
1516
  );
1147
1517
 
1148
1518
  INSERT OR IGNORE INTO log_tags (log_entry_id, tag)
1149
- SELECT selected.value, addition.value
1150
- FROM json_each(NEW.attrs, '$.__plurnk_curation.ids') selected
1519
+ SELECT json_extract(selected.value, '$.id'), addition.value
1520
+ FROM json_each(NEW.attrs, '$.__plurnk_curation.targets') selected
1151
1521
  CROSS JOIN json_each(NEW.attrs, '$.__plurnk_curation.add') addition;
1152
1522
 
1153
1523
  UPDATE log_entries
@@ -1157,18 +1527,18 @@ END;
1157
1527
 
1158
1528
  -- Column-scoped immutability: the original action's identity and target never
1159
1529
  -- change; the proposal lifecycle is allowed to mutate state, outcome,
1160
- -- status_rx, rx, expanded. Keep attrs separate so the curation trigger's one
1530
+ -- status_rx, rx, folded. Keep attrs separate so the curation trigger's one
1161
1531
  -- private-payload removal cannot exempt changes to any other core column.
1162
1532
  CREATE TRIGGER IF NOT EXISTS log_entries_immutable_core
1163
1533
  BEFORE UPDATE OF
1164
1534
  worker_id, loop_id, turn_id, sequence, at, origin, source, model_call_id,
1165
- op, suffix, signal,
1535
+ op, delimiter, signal,
1166
1536
  scheme, username, password, hostname,
1167
1537
  port, pathname, query, fragment,
1168
1538
  lineMarker, tx, mimetype_tx, mimetype_rx
1169
1539
  ON log_entries
1170
1540
  BEGIN
1171
- SELECT RAISE(ABORT, 'log_entries core fields are immutable; only state/outcome/status_rx/rx/expanded may change');
1541
+ SELECT RAISE(ABORT, 'log_entries core fields are immutable; only state/outcome/status_rx/rx/folded may change');
1172
1542
  END;
1173
1543
 
1174
1544
  CREATE TRIGGER IF NOT EXISTS log_entries_immutable_attrs
@@ -1202,7 +1572,7 @@ WHEN NEW.ambient_event_id IS NULL
1202
1572
  AND (NEW.scheme IS NULL OR NEW.scheme != 'plurnk')
1203
1573
  AND (NEW.scheme IS NULL OR NEW.scheme != 'worker' OR NEW.hostname IS NULL OR NEW.hostname = 'plurnk')
1204
1574
  AND (
1205
- NEW.origin != 'plurnk'
1575
+ NEW.origin != '_plurnk'
1206
1576
  OR EXISTS (SELECT 1 FROM workers w WHERE w.id = NEW.worker_id AND w.name = 'plurnk')
1207
1577
  )
1208
1578
  BEGIN
@@ -1236,7 +1606,7 @@ WHEN OLD.state = 'proposed'
1236
1606
  AND (NEW.scheme IS NULL OR NEW.scheme != 'plurnk')
1237
1607
  AND (NEW.scheme IS NULL OR NEW.scheme != 'worker' OR NEW.hostname IS NULL OR NEW.hostname = 'plurnk')
1238
1608
  AND (
1239
- NEW.origin != 'plurnk'
1609
+ NEW.origin != '_plurnk'
1240
1610
  OR EXISTS (SELECT 1 FROM workers w WHERE w.id = NEW.worker_id AND w.name = 'plurnk')
1241
1611
  )
1242
1612
  BEGIN
@@ -1258,6 +1628,26 @@ BEGIN
1258
1628
  UPDATE log_entries SET ambient_event_id = last_insert_rowid() WHERE id = NEW.id;
1259
1629
  END;
1260
1630
 
1631
+ -- {§client-interactions}: the durable discoverable half of a client-owned
1632
+ -- interaction. The process-local lifecycle owner holds the awaiting callable;
1633
+ -- this row holds only presentation and routing facts. Settlement deletes the
1634
+ -- row, so client response payloads and owner-private continuation state are
1635
+ -- never copied into persistence.
1636
+ CREATE TABLE IF NOT EXISTS client_interactions (
1637
+ id INTEGER NOT NULL PRIMARY KEY,
1638
+ worker_id INTEGER NOT NULL,
1639
+ loop_id INTEGER NOT NULL,
1640
+ turn_id INTEGER NOT NULL,
1641
+ request TEXT NOT NULL CHECK (json_valid(request) AND json_type(request) = 'object'),
1642
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
1643
+ FOREIGN KEY (worker_id) REFERENCES workers(id) ON DELETE CASCADE,
1644
+ FOREIGN KEY (loop_id) REFERENCES loops(id) ON DELETE CASCADE,
1645
+ FOREIGN KEY (turn_id) REFERENCES turns(id) ON DELETE CASCADE
1646
+ ) STRICT;
1647
+
1648
+ CREATE INDEX IF NOT EXISTS client_interactions_worker_id_id
1649
+ ON client_interactions (worker_id, id);
1650
+
1261
1651
  -- subscriptions
1262
1652
  -- Durable subscription lifecycle per SPEC {§subscriptions}. The row records what
1263
1653
  -- the worker holds and routes cancellation to a separate process-local callable;