@plurnk/plurnk-service 1.7.0 → 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 (280) hide show
  1. package/.env.defaults +29 -42
  2. package/INSTALL.md +49 -10
  3. package/README.md +11 -1
  4. package/SPEC.md +631 -279
  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/Dispatcher.d.ts +7 -6
  28. package/dist/core/Dispatcher.d.ts.map +1 -1
  29. package/dist/core/Dispatcher.js +34 -88
  30. package/dist/core/Dispatcher.js.map +1 -1
  31. package/dist/core/Engine.d.ts +11 -8
  32. package/dist/core/Engine.d.ts.map +1 -1
  33. package/dist/core/Engine.js +45 -27
  34. package/dist/core/Engine.js.map +1 -1
  35. package/dist/core/Engine.sql +112 -107
  36. package/dist/core/ExecutorRegistry.d.ts +8 -2
  37. package/dist/core/ExecutorRegistry.d.ts.map +1 -1
  38. package/dist/core/ExecutorRegistry.js +6 -3
  39. package/dist/core/ExecutorRegistry.js.map +1 -1
  40. package/dist/core/HostPaths.d.ts +29 -0
  41. package/dist/core/HostPaths.d.ts.map +1 -0
  42. package/dist/core/HostPaths.js +74 -0
  43. package/dist/core/HostPaths.js.map +1 -0
  44. package/dist/core/LegacyHome.d.ts +10 -0
  45. package/dist/core/LegacyHome.d.ts.map +1 -0
  46. package/dist/core/LegacyHome.js +170 -0
  47. package/dist/core/LegacyHome.js.map +1 -0
  48. package/dist/core/LogBody.d.ts +1 -1
  49. package/dist/core/LogBody.d.ts.map +1 -1
  50. package/dist/core/LogBody.js +2 -2
  51. package/dist/core/LogBody.js.map +1 -1
  52. package/dist/core/LogEntryProjection.js +1 -1
  53. package/dist/core/LogEntryProjection.js.map +1 -1
  54. package/dist/core/LogVisibility.d.ts +28 -0
  55. package/dist/core/LogVisibility.d.ts.map +1 -0
  56. package/dist/core/LogVisibility.js +215 -0
  57. package/dist/core/LogVisibility.js.map +1 -0
  58. package/dist/core/LoopLifecycle.d.ts +1 -0
  59. package/dist/core/LoopLifecycle.d.ts.map +1 -1
  60. package/dist/core/LoopLifecycle.js +8 -0
  61. package/dist/core/LoopLifecycle.js.map +1 -1
  62. package/dist/core/LoopLifecycle.sql +10 -0
  63. package/dist/core/OperatorConfig.d.ts +6 -0
  64. package/dist/core/OperatorConfig.d.ts.map +1 -0
  65. package/dist/core/OperatorConfig.js +92 -0
  66. package/dist/core/OperatorConfig.js.map +1 -0
  67. package/dist/core/OverflowTurn.d.ts +11 -0
  68. package/dist/core/OverflowTurn.d.ts.map +1 -0
  69. package/dist/core/OverflowTurn.js +77 -0
  70. package/dist/core/OverflowTurn.js.map +1 -0
  71. package/dist/core/Owner.js +2 -2
  72. package/dist/core/Owner.js.map +1 -1
  73. package/dist/core/PacketBuilder.d.ts +3 -15
  74. package/dist/core/PacketBuilder.d.ts.map +1 -1
  75. package/dist/core/PacketBuilder.js +48 -121
  76. package/dist/core/PacketBuilder.js.map +1 -1
  77. package/dist/core/ProblemLog.js +1 -1
  78. package/dist/core/ProblemLog.js.map +1 -1
  79. package/dist/core/ProposalLifecycle.d.ts.map +1 -1
  80. package/dist/core/ProposalLifecycle.js +3 -23
  81. package/dist/core/ProposalLifecycle.js.map +1 -1
  82. package/dist/core/ProviderInstantiate.d.ts +9 -5
  83. package/dist/core/ProviderInstantiate.d.ts.map +1 -1
  84. package/dist/core/ProviderInstantiate.js +99 -104
  85. package/dist/core/ProviderInstantiate.js.map +1 -1
  86. package/dist/core/ResourceMutations.d.ts.map +1 -1
  87. package/dist/core/ResourceMutations.js +5 -3
  88. package/dist/core/ResourceMutations.js.map +1 -1
  89. package/dist/core/SchemeRegistry.d.ts +0 -1
  90. package/dist/core/SchemeRegistry.d.ts.map +1 -1
  91. package/dist/core/SchemeRegistry.js +7 -11
  92. package/dist/core/SchemeRegistry.js.map +1 -1
  93. package/dist/core/ToolResources.d.ts +19 -0
  94. package/dist/core/ToolResources.d.ts.map +1 -0
  95. package/dist/core/ToolResources.js +122 -0
  96. package/dist/core/ToolResources.js.map +1 -0
  97. package/dist/core/Turn.d.ts +27 -0
  98. package/dist/core/Turn.d.ts.map +1 -0
  99. package/dist/core/Turn.js +43 -0
  100. package/dist/core/Turn.js.map +1 -0
  101. package/dist/core/Turn.sql +72 -0
  102. package/dist/core/TurnOps.d.ts +7 -0
  103. package/dist/core/TurnOps.d.ts.map +1 -0
  104. package/dist/core/TurnOps.js +63 -0
  105. package/dist/core/TurnOps.js.map +1 -0
  106. package/dist/core/TurnRunner.d.ts +12 -4
  107. package/dist/core/TurnRunner.d.ts.map +1 -1
  108. package/dist/core/TurnRunner.js +1045 -898
  109. package/dist/core/TurnRunner.js.map +1 -1
  110. package/dist/core/WorkerName.d.ts +1 -1
  111. package/dist/core/WorkerName.d.ts.map +1 -1
  112. package/dist/core/env-defaults.d.ts.map +1 -1
  113. package/dist/core/env-defaults.js +7 -5
  114. package/dist/core/env-defaults.js.map +1 -1
  115. package/dist/core/fork.d.ts.map +1 -1
  116. package/dist/core/fork.js +15 -4
  117. package/dist/core/fork.js.map +1 -1
  118. package/dist/core/fork.sql +57 -27
  119. package/dist/core/git-env.d.ts +0 -1
  120. package/dist/core/git-env.d.ts.map +1 -1
  121. package/dist/core/git-env.js +0 -3
  122. package/dist/core/git-env.js.map +1 -1
  123. package/dist/core/git-membership.d.ts.map +1 -1
  124. package/dist/core/git-membership.js +10 -26
  125. package/dist/core/git-membership.js.map +1 -1
  126. package/dist/core/git-state.d.ts.map +1 -1
  127. package/dist/core/git-state.js +13 -14
  128. package/dist/core/git-state.js.map +1 -1
  129. package/dist/core/operation-target-groups.d.ts +3 -0
  130. package/dist/core/operation-target-groups.d.ts.map +1 -0
  131. package/dist/core/operation-target-groups.js +53 -0
  132. package/dist/core/operation-target-groups.js.map +1 -0
  133. package/dist/core/packet-inject.d.ts +0 -1
  134. package/dist/core/packet-inject.d.ts.map +1 -1
  135. package/dist/core/packet-inject.js +6 -13
  136. package/dist/core/packet-inject.js.map +1 -1
  137. package/dist/core/packet-wire.d.ts +4 -0
  138. package/dist/core/packet-wire.d.ts.map +1 -1
  139. package/dist/core/packet-wire.js +208 -44
  140. package/dist/core/packet-wire.js.map +1 -1
  141. package/dist/core/plurnk-uri.d.ts +1 -1
  142. package/dist/core/plurnk-uri.d.ts.map +1 -1
  143. package/dist/core/plurnk-uri.js +3 -3
  144. package/dist/core/plurnk-uri.js.map +1 -1
  145. package/dist/core/teaching.d.ts.map +1 -1
  146. package/dist/core/teaching.js +4 -5
  147. package/dist/core/teaching.js.map +1 -1
  148. package/dist/core/worker-settings.d.ts +9 -0
  149. package/dist/core/worker-settings.d.ts.map +1 -0
  150. package/dist/core/worker-settings.js +25 -0
  151. package/dist/core/worker-settings.js.map +1 -0
  152. package/dist/core/workspace-settings.d.ts +0 -11
  153. package/dist/core/workspace-settings.d.ts.map +1 -1
  154. package/dist/core/workspace-settings.js +3 -47
  155. package/dist/core/workspace-settings.js.map +1 -1
  156. package/dist/digest/Digest.d.ts.map +1 -1
  157. package/dist/digest/Digest.js +164 -92
  158. package/dist/digest/Digest.js.map +1 -1
  159. package/dist/digest/digest.sql +8 -2
  160. package/dist/observe/genai.d.ts +9 -0
  161. package/dist/observe/genai.d.ts.map +1 -0
  162. package/dist/observe/genai.js +31 -0
  163. package/dist/observe/genai.js.map +1 -0
  164. package/dist/observe/spans.d.ts +3 -3
  165. package/dist/observe/spans.d.ts.map +1 -1
  166. package/dist/observe/spans.js +20 -5
  167. package/dist/observe/spans.js.map +1 -1
  168. package/dist/schemes/EffectPolicy.d.ts +3 -0
  169. package/dist/schemes/EffectPolicy.d.ts.map +1 -1
  170. package/dist/schemes/EffectPolicy.js +38 -6
  171. package/dist/schemes/EffectPolicy.js.map +1 -1
  172. package/dist/schemes/Exec.d.ts.map +1 -1
  173. package/dist/schemes/Exec.js +55 -13
  174. package/dist/schemes/Exec.js.map +1 -1
  175. package/dist/schemes/Log.d.ts +6 -2
  176. package/dist/schemes/Log.d.ts.map +1 -1
  177. package/dist/schemes/Log.js +73 -22
  178. package/dist/schemes/Log.js.map +1 -1
  179. package/dist/schemes/Log.sql +25 -7
  180. package/dist/schemes/Prompt.d.ts.map +1 -1
  181. package/dist/schemes/Prompt.js +1 -2
  182. package/dist/schemes/Prompt.js.map +1 -1
  183. package/dist/schemes/QuestionTool.d.ts +10 -0
  184. package/dist/schemes/QuestionTool.d.ts.map +1 -0
  185. package/dist/schemes/QuestionTool.js +81 -0
  186. package/dist/schemes/QuestionTool.js.map +1 -0
  187. package/dist/schemes/Skill.d.ts.map +1 -1
  188. package/dist/schemes/Skill.js +4 -1
  189. package/dist/schemes/Skill.js.map +1 -1
  190. package/dist/schemes/Worker.d.ts.map +1 -1
  191. package/dist/schemes/Worker.js +20 -3
  192. package/dist/schemes/Worker.js.map +1 -1
  193. package/dist/schemes/_entry-crud.sql +19 -1
  194. package/dist/schemes/_entry-find.d.ts +5 -3
  195. package/dist/schemes/_entry-find.d.ts.map +1 -1
  196. package/dist/schemes/_entry-find.js +102 -22
  197. package/dist/schemes/_entry-find.js.map +1 -1
  198. package/dist/schemes/_entry-find.sql +8 -6
  199. package/dist/schemes/_entry-graph.sql +2 -6
  200. package/dist/schemes/_entry-manifest.d.ts.map +1 -1
  201. package/dist/schemes/_entry-manifest.js +12 -1
  202. package/dist/schemes/_entry-manifest.js.map +1 -1
  203. package/dist/schemes/_search-index.d.ts.map +1 -1
  204. package/dist/schemes/_search-index.js +37 -25
  205. package/dist/schemes/_search-index.js.map +1 -1
  206. package/dist/server/Daemon.d.ts +58 -7
  207. package/dist/server/Daemon.d.ts.map +1 -1
  208. package/dist/server/Daemon.js +551 -113
  209. package/dist/server/Daemon.js.map +1 -1
  210. package/dist/server/DrainSupervisor.d.ts +12 -9
  211. package/dist/server/DrainSupervisor.d.ts.map +1 -1
  212. package/dist/server/DrainSupervisor.js +59 -10
  213. package/dist/server/DrainSupervisor.js.map +1 -1
  214. package/dist/server/client-input.d.ts +5 -2
  215. package/dist/server/client-input.d.ts.map +1 -1
  216. package/dist/server/client-input.js +53 -34
  217. package/dist/server/client-input.js.map +1 -1
  218. package/dist/server/dispatch-as-plurnk.d.ts.map +1 -1
  219. package/dist/server/dispatch-as-plurnk.js +28 -6
  220. package/dist/server/dispatch-as-plurnk.js.map +1 -1
  221. package/dist/server/drain.sql +26 -11
  222. package/dist/server/envelope.d.ts +1 -1
  223. package/dist/server/envelope.d.ts.map +1 -1
  224. package/dist/server/envelope.js +4 -4
  225. package/dist/server/envelope.js.map +1 -1
  226. package/dist/server/envelope.sql +25 -1
  227. package/dist/server/lifecycle-recovery.sql +11 -1
  228. package/dist/server/logEntry.d.ts +2 -1
  229. package/dist/server/logEntry.d.ts.map +1 -1
  230. package/dist/server/logEntry.js +3 -2
  231. package/dist/server/logEntry.js.map +1 -1
  232. package/dist/server/logEntry.sql +7 -0
  233. package/dist/server/loop-model.d.ts +2 -2
  234. package/dist/server/loop-model.d.ts.map +1 -1
  235. package/dist/server/loop-model.js +23 -33
  236. package/dist/server/loop-model.js.map +1 -1
  237. package/dist/server/loopDocs.d.ts.map +1 -1
  238. package/dist/server/loopDocs.js +61 -10
  239. package/dist/server/loopDocs.js.map +1 -1
  240. package/dist/server/loopDocs.sql +4 -2
  241. package/dist/server/model-catalog.d.ts +3 -0
  242. package/dist/server/model-catalog.d.ts.map +1 -0
  243. package/dist/server/model-catalog.js +83 -0
  244. package/dist/server/model-catalog.js.map +1 -0
  245. package/dist/server/model-route.d.ts +7 -0
  246. package/dist/server/model-route.d.ts.map +1 -0
  247. package/dist/server/model-route.js +47 -0
  248. package/dist/server/model-route.js.map +1 -0
  249. package/dist/server/module-discovery.d.ts +13 -0
  250. package/dist/server/module-discovery.d.ts.map +1 -0
  251. package/dist/server/module-discovery.js +78 -0
  252. package/dist/server/module-discovery.js.map +1 -0
  253. package/dist/server/skillDocs.d.ts +9 -0
  254. package/dist/server/skillDocs.d.ts.map +1 -0
  255. package/dist/server/skillDocs.js +212 -0
  256. package/dist/server/skillDocs.js.map +1 -0
  257. package/dist/server/skillDocs.sql +14 -0
  258. package/dist/service.d.ts.map +1 -1
  259. package/dist/service.js +182 -123
  260. package/dist/service.js.map +1 -1
  261. package/migrations/001_schema.sql +424 -65
  262. package/package.json +37 -37
  263. package/dist/core/ExecutableTools.d.ts +0 -11
  264. package/dist/core/ExecutableTools.d.ts.map +0 -1
  265. package/dist/core/ExecutableTools.js +0 -49
  266. package/dist/core/ExecutableTools.js.map +0 -1
  267. package/dist/core/JournalTurn.d.ts +0 -9
  268. package/dist/core/JournalTurn.d.ts.map +0 -1
  269. package/dist/core/JournalTurn.js +0 -14
  270. package/dist/core/JournalTurn.js.map +0 -1
  271. package/dist/core/JournalTurn.sql +0 -10
  272. package/dist/core/git-iso.d.ts +0 -13
  273. package/dist/core/git-iso.d.ts.map +0 -1
  274. package/dist/core/git-iso.js +0 -267
  275. package/dist/core/git-iso.js.map +0 -1
  276. package/dist/core/search-gate.d.ts +0 -17
  277. package/dist/core/search-gate.d.ts.map +0 -1
  278. package/dist/core/search-gate.js +0 -77
  279. package/dist/core/search-gate.js.map +0 -1
  280. package/dist/digest/Digest.sql +0 -1
@@ -30,6 +30,21 @@ CREATE TABLE IF NOT EXISTS workspace_module_state (
30
30
  FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE
31
31
  ) STRICT;
32
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
+
33
48
  -- workers
34
49
  CREATE TABLE IF NOT EXISTS workers (
35
50
  id INTEGER NOT NULL PRIMARY KEY,
@@ -41,16 +56,31 @@ CREATE TABLE IF NOT EXISTS workers (
41
56
  -- independent databases reuse local integer ids.
42
57
  provider_identity TEXT NOT NULL DEFAULT (lower(hex(randomblob(16))))
43
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')),
44
67
  -- workers fork via parent_worker_id; workspaces carry no parent — {§machine-processes-no-fork-workspace}
45
68
  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')),
69
+ origin TEXT NOT NULL DEFAULT 'client' CHECK (origin IN ('model', 'client', '_plurnk')),
47
70
  -- {§methods-model-worker}: durable identity for the workspace's stable
48
71
  -- default conversation; unrelated to its human-facing, reclaimable name.
49
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)),
50
79
  -- {§env-delta-log-pull}: monotonic observation progress, not a private world snapshot.
51
80
  -- NULL means the worker has not established its first-turn baseline yet.
52
81
  ambient_event_cursor INTEGER CHECK (ambient_event_cursor IS NULL OR ambient_event_cursor >= 0),
53
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)),
54
84
  FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE,
55
85
  FOREIGN KEY (parent_worker_id) REFERENCES workers(id) ON DELETE CASCADE
56
86
  ) STRICT;
@@ -129,8 +159,11 @@ CREATE TABLE IF NOT EXISTS loops (
129
159
  status INTEGER NOT NULL DEFAULT 102 CHECK (status IN (100, 102, 200, 202, 413, 429, 499, 500, 504, 508)),
130
160
  prompt TEXT NOT NULL,
131
161
  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)),
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')),
134
167
  max_turns INTEGER NOT NULL DEFAULT 50 CHECK (max_turns >= -1),
135
168
  -- {§methods-loop-run-open-paths}: the initial prompt frame's selected paths,
136
169
  -- held here until turn 1 materializes that frame (string[] JSON).
@@ -145,6 +178,9 @@ CREATE TABLE IF NOT EXISTS loops (
145
178
  -- {§loop-terminal-authorship}: 'cancel' names an external loop.cancel;
146
179
  -- NULL covers model terminals and engine verdicts whose result carries the story.
147
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
+ ),
148
184
  CONSTRAINT loops_terminal_result_contract CHECK (
149
185
  CASE
150
186
  WHEN status IN (100, 102, 202) THEN terminal_result IS NULL
@@ -191,7 +227,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS loops_worker_id_sequence ON loops (worker_id,
191
227
  CREATE UNIQUE INDEX IF NOT EXISTS loops_orphan_source_loop_id ON loops (orphan_source_loop_id);
192
228
  -- {§worker-scheme}: a loop crossing into a terminal status stamps terminated_at, so sibling
193
229
  -- 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,
230
+ -- death-path (SEND, overflow recovery, max-turns, strike, KILL). The stamp updates terminated_at,
195
231
  -- never status, so it cannot re-fire this trigger. Terminals: 200 done · 413 budget ·
196
232
  -- 429 turn-ceiling · 499 cancel · 500 fail · 504 wall-clock timeout · 508 runaway. (202 = parked/sleeping, NOT terminal.)
197
233
  CREATE TRIGGER IF NOT EXISTS loops_stamp_terminated_at
@@ -229,7 +265,15 @@ CREATE TABLE IF NOT EXISTS turns (
229
265
  loop_id INTEGER NOT NULL,
230
266
  sequence INTEGER NOT NULL CHECK (sequence >= 1),
231
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')),
232
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')),
233
277
  -- Provider-derived curation calibration for this turn; NULL when input
234
278
  -- capacity is unknown. {§tokenomics-client-gauge}
235
279
  usage_curation_budget INTEGER CHECK (usage_curation_budget IS NULL OR usage_curation_budget >= 1),
@@ -264,16 +308,59 @@ CREATE TABLE IF NOT EXISTS turns (
264
308
  END
265
309
  ),
266
310
  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)),
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
+ ),
271
328
  FOREIGN KEY (loop_id) REFERENCES loops(id) ON DELETE CASCADE
272
329
  ) STRICT;
273
330
 
274
331
  CREATE UNIQUE INDEX IF NOT EXISTS turns_loop_id_sequence ON turns (loop_id, sequence);
275
332
  CREATE INDEX IF NOT EXISTS turns_timestamp ON turns (timestamp);
276
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
+
277
364
  -- One logical provider.generate call. Emission attempts and BARE inferences
278
365
  -- share response/failure evidence and cardinal physical request accounting;
279
366
  -- operation-specific semantics live in their specializing relations.
@@ -321,6 +408,17 @@ CREATE TABLE IF NOT EXISTS model_calls (
321
408
 
322
409
  CREATE INDEX IF NOT EXISTS model_calls_turn_id ON model_calls (turn_id, sequence);
323
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
+
324
422
  CREATE TRIGGER IF NOT EXISTS model_calls_request_identity_immutable
325
423
  BEFORE UPDATE OF turn_id, sequence, kind, attributions, timestamp ON model_calls
326
424
  BEGIN
@@ -532,9 +630,9 @@ BEGIN
532
630
  END;
533
631
 
534
632
  -- 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.
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.
538
636
  -- A building row is unattached and safely replaceable after interruption.
539
637
  CREATE TABLE IF NOT EXISTS derivations (
540
638
  id INTEGER NOT NULL PRIMARY KEY,
@@ -543,6 +641,14 @@ CREATE TABLE IF NOT EXISTS derivations (
543
641
  disposition TEXT CHECK (disposition IN ('vector', 'lexical', 'excluded', 'nonsemantic', 'failed')),
544
642
  reason TEXT,
545
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
+ ),
546
652
  CHECK ((state = 'building' AND disposition IS NULL) OR (state = 'complete' AND disposition IS NOT NULL))
547
653
  ) STRICT;
548
654
 
@@ -572,11 +678,6 @@ CREATE TABLE IF NOT EXISTS entries (
572
678
  -- == members; 'client'/'constraint' (model-created, add-glob) are not git's to reclaim.
573
679
  -- NULL = not a file member (other schemes don't carry origin).
574
680
  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,
580
681
  -- SPEC {§membership-change-gated-sync} — the per-member sync stat-detect:
581
682
  -- "<mtimeMs>:<size>" of the disk file at its last materialization, or `absent`
582
683
  -- after an observed deletion. The pre-turn
@@ -584,13 +685,13 @@ CREATE TABLE IF NOT EXISTS entries (
584
685
  -- signature changed; an unchanged member is a no-op. NULL = never synced.
585
686
  synced_sig TEXT,
586
687
  -- 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.
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.
589
691
  updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
590
692
  CHECK (workspace_id IS NOT NULL),
591
693
  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)
694
+ FOREIGN KEY (owner_id) REFERENCES workers(id) ON DELETE CASCADE
594
695
  ) STRICT;
595
696
 
596
697
  -- {§entry-owner} / {§stream-owner-scoped} — owner is the sole visibility and identity axis.
@@ -610,6 +711,9 @@ CREATE TABLE IF NOT EXISTS entry_channels (
610
711
  -- Content identity: sha256 of content, stamped at static writes; streamed
611
712
  -- appends leave it NULL. Curation weight remains model-independent.
612
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,
613
717
  state TEXT NOT NULL DEFAULT 'static' CHECK (state IN ('static', 'active', 'closed', 'errored')),
614
718
  -- Exact terminal producer evidence for this representation channel. NULL is
615
719
  -- the ordinary implicit {status:200}; selection/projection metadata belongs
@@ -642,12 +746,26 @@ CREATE TABLE IF NOT EXISTS entry_channels (
642
746
  END
643
747
  ),
644
748
  PRIMARY KEY (entry_id, name),
645
- 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)
646
751
  ) STRICT, WITHOUT ROWID;
647
752
 
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.
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.
651
769
  CREATE TRIGGER IF NOT EXISTS entries_touch_on_channel_write
652
770
  AFTER INSERT ON entry_channels
653
771
  BEGIN
@@ -655,7 +773,7 @@ BEGIN
655
773
  END;
656
774
 
657
775
  CREATE TRIGGER IF NOT EXISTS entries_touch_on_channel_update
658
- AFTER UPDATE ON entry_channels
776
+ AFTER UPDATE OF content, mimetype, weight, state, producer_result ON entry_channels
659
777
  BEGIN
660
778
  UPDATE entries SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = NEW.entry_id;
661
779
  END;
@@ -731,7 +849,8 @@ CREATE TABLE IF NOT EXISTS derivation_embeddings (
731
849
  -- status⊥state: status is the HTTP outcome, state is where in the
732
850
  -- lifecycle the entry sits. Most rows write 'resolved' directly;
733
851
  -- proposing schemes transition 'proposed' → resolved/failed/cancelled.
734
- -- 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.
735
854
  CREATE TABLE IF NOT EXISTS log_entries (
736
855
  id INTEGER NOT NULL PRIMARY KEY,
737
856
  version INTEGER NOT NULL DEFAULT 0 CHECK (version >= 0),
@@ -741,7 +860,7 @@ CREATE TABLE IF NOT EXISTS log_entries (
741
860
  turn_id INTEGER NOT NULL,
742
861
  sequence INTEGER NOT NULL CHECK (sequence >= 1),
743
862
  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')),
863
+ origin TEXT NOT NULL CHECK (origin IN ('model', 'client', '_plurnk', 'plugin')),
745
864
  -- {§env-delta-attribution}: a worker:// name or stable subsystem token ('file');
746
865
  -- NULL = the owning worker itself, rendered without causal attribution.
747
866
  source TEXT,
@@ -750,21 +869,22 @@ CREATE TABLE IF NOT EXISTS log_entries (
750
869
  ambient_event_id INTEGER REFERENCES ambient_events(id),
751
870
  -- Search derivation attached to this durable log result, when available.
752
871
  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.
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.
755
875
  model_call_id INTEGER REFERENCES model_calls(id),
756
876
 
757
877
  -- 'error' is an ACTIONLESS row ({§operation-results} — errors are log items): a parse failure that
758
878
  -- produced no op still records a log entry (op='error', status_rx≥400, no target) so the model
759
879
  -- 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}).
880
+ -- Exact producer source and rejected-attempt artifacts carry NULL here
881
+ -- ({§turn-ops-entry}, {§rejected-emission-entry}).
762
882
  -- No op enum here: the grammar op set is grammar's contract (PlurnkOp), and this column is written
763
883
  -- only by the PlurnkOp-typed engine (grammar ops), service row selectors, or NULL for no op.
764
884
  -- A SQL enum would be a hand-copy of grammar's op list that silently goes stale on every new verb
765
885
  -- (it did — FORK/WORK). Validity lives at the parse + type layer, not duplicated in DDL.
766
886
  op TEXT,
767
- suffix TEXT NOT NULL DEFAULT '',
887
+ delimiter TEXT NOT NULL DEFAULT '',
768
888
  signal TEXT CHECK (signal IS NULL OR json_valid(signal)),
769
889
 
770
890
  scheme TEXT CHECK (scheme IS NULL OR length(scheme) > 0),
@@ -795,11 +915,16 @@ CREATE TABLE IF NOT EXISTS log_entries (
795
915
  outcome TEXT,
796
916
  attrs TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(attrs)),
797
917
 
798
- 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'),
799
920
 
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'),
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'),
803
928
 
804
929
  FOREIGN KEY (worker_id) REFERENCES workers(id) ON DELETE CASCADE,
805
930
  FOREIGN KEY (loop_id) REFERENCES loops(id) ON DELETE CASCADE,
@@ -817,6 +942,108 @@ CREATE UNIQUE INDEX IF NOT EXISTS log_entries_worker_ambient_event
817
942
  ON log_entries (worker_id, ambient_event_id)
818
943
  WHERE ambient_event_id IS NOT NULL;
819
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
+
820
1047
  CREATE TRIGGER IF NOT EXISTS log_entries_model_call_valid
821
1048
  BEFORE INSERT ON log_entries
822
1049
  WHEN NEW.model_call_id IS NOT NULL
@@ -831,7 +1058,7 @@ WHEN NEW.model_call_id IS NOT NULL
831
1058
  OR (
832
1059
  call.kind = 'emission'
833
1060
  AND NEW.op IS NULL
834
- AND json_extract(NEW.attrs, '$.kind') = 'model_emission'
1061
+ AND json_extract(NEW.attrs, '$.kind') IN ('turnOps', 'emissionAttempt')
835
1062
  )
836
1063
  )
837
1064
  )
@@ -935,14 +1162,61 @@ BEGIN
935
1162
  AND source_record_id = NEW.id;
936
1163
  END;
937
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
+
938
1209
  -- Successful OPEN/FOLD rows are durable curation events even though their
939
1210
  -- ordinary packet projection is suppressed ({§fold-open-meta-operations}).
940
- -- 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
941
1212
  -- broad selector never collapses into the lossy fact `matched: N`.
942
1213
  CREATE TABLE IF NOT EXISTS log_curation_effects (
943
1214
  operation_log_entry_id INTEGER NOT NULL,
944
1215
  target_log_entry_id INTEGER NOT NULL,
945
- 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'),
946
1220
  tags_added TEXT NOT NULL DEFAULT '[]'
947
1221
  CHECK (json_valid(tags_added) AND json_type(tags_added) = 'array'),
948
1222
  tags_removed TEXT NOT NULL DEFAULT '[]'
@@ -972,6 +1246,39 @@ BEGIN
972
1246
  AND operation.status_rx < 400
973
1247
  AND operation.worker_id = target.worker_id
974
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
+ )
975
1282
  OR EXISTS (
976
1283
  SELECT 1
977
1284
  FROM (
@@ -1027,7 +1334,7 @@ BEGIN
1027
1334
  END;
1028
1335
 
1029
1336
  -- 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
1337
+ -- their before/after visibility and tag deltas, and the
1031
1338
  -- resulting classifications. Trigger failure rolls the operation row and all
1032
1339
  -- effects back together. The private plan is erased before INSERT returns.
1033
1340
  CREATE TRIGGER IF NOT EXISTS log_entries_apply_curation
@@ -1037,26 +1344,70 @@ WHEN NEW.op IN ('OPEN', 'FOLD')
1037
1344
  AND json_type(NEW.attrs, '$.__plurnk_curation') = 'object'
1038
1345
  BEGIN
1039
1346
  SELECT CASE WHEN
1040
- COALESCE(json_type(NEW.attrs, '$.__plurnk_curation.ids'), '') != 'array'
1347
+ COALESCE(json_type(NEW.attrs, '$.__plurnk_curation.targets'), '') != 'array'
1041
1348
  OR COALESCE(json_type(NEW.attrs, '$.__plurnk_curation.add'), '') != 'array'
1042
1349
  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
1350
+ OR json_array_length(NEW.attrs, '$.__plurnk_curation.targets') = 0
1046
1351
  OR EXISTS (
1047
- SELECT 1 FROM json_each(NEW.attrs, '$.__plurnk_curation.ids')
1048
- 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
+ )
1049
1396
  )
1050
1397
  OR (
1051
- SELECT COUNT(*) FROM json_each(NEW.attrs, '$.__plurnk_curation.ids')
1398
+ SELECT COUNT(*) FROM json_each(NEW.attrs, '$.__plurnk_curation.targets')
1052
1399
  ) != (
1053
- 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')
1054
1402
  )
1055
1403
  OR EXISTS (
1056
1404
  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
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'))
1060
1411
  )
1061
1412
  OR EXISTS (
1062
1413
  SELECT 1
@@ -1107,14 +1458,16 @@ BEGIN
1107
1458
  INSERT INTO log_curation_effects (
1108
1459
  operation_log_entry_id,
1109
1460
  target_log_entry_id,
1110
- expanded_before,
1461
+ folded_before,
1462
+ folded_after,
1111
1463
  tags_added,
1112
1464
  tags_removed
1113
1465
  )
1114
1466
  SELECT
1115
1467
  NEW.id,
1116
1468
  target.id,
1117
- target.expanded,
1469
+ json_extract(selected.value, '$.before'),
1470
+ json_extract(selected.value, '$.after'),
1118
1471
  COALESCE((
1119
1472
  SELECT json_group_array(ordered.tag)
1120
1473
  FROM (
@@ -1139,26 +1492,32 @@ BEGIN
1139
1492
  ORDER BY removal.value
1140
1493
  ) ordered
1141
1494
  ), '[]')
1142
- FROM json_each(NEW.attrs, '$.__plurnk_curation.ids') selected
1143
- 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');
1144
1497
 
1145
1498
  UPDATE log_entries
1146
- 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
+ )
1147
1504
  WHERE id IN (
1148
- 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')
1149
1507
  );
1150
1508
 
1151
1509
  DELETE FROM log_tags
1152
1510
  WHERE log_entry_id IN (
1153
- 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')
1154
1513
  )
1155
1514
  AND tag IN (
1156
1515
  SELECT value FROM json_each(NEW.attrs, '$.__plurnk_curation.remove')
1157
1516
  );
1158
1517
 
1159
1518
  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
1519
+ SELECT json_extract(selected.value, '$.id'), addition.value
1520
+ FROM json_each(NEW.attrs, '$.__plurnk_curation.targets') selected
1162
1521
  CROSS JOIN json_each(NEW.attrs, '$.__plurnk_curation.add') addition;
1163
1522
 
1164
1523
  UPDATE log_entries
@@ -1168,18 +1527,18 @@ END;
1168
1527
 
1169
1528
  -- Column-scoped immutability: the original action's identity and target never
1170
1529
  -- change; the proposal lifecycle is allowed to mutate state, outcome,
1171
- -- 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
1172
1531
  -- private-payload removal cannot exempt changes to any other core column.
1173
1532
  CREATE TRIGGER IF NOT EXISTS log_entries_immutable_core
1174
1533
  BEFORE UPDATE OF
1175
1534
  worker_id, loop_id, turn_id, sequence, at, origin, source, model_call_id,
1176
- op, suffix, signal,
1535
+ op, delimiter, signal,
1177
1536
  scheme, username, password, hostname,
1178
1537
  port, pathname, query, fragment,
1179
1538
  lineMarker, tx, mimetype_tx, mimetype_rx
1180
1539
  ON log_entries
1181
1540
  BEGIN
1182
- 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');
1183
1542
  END;
1184
1543
 
1185
1544
  CREATE TRIGGER IF NOT EXISTS log_entries_immutable_attrs
@@ -1213,7 +1572,7 @@ WHEN NEW.ambient_event_id IS NULL
1213
1572
  AND (NEW.scheme IS NULL OR NEW.scheme != 'plurnk')
1214
1573
  AND (NEW.scheme IS NULL OR NEW.scheme != 'worker' OR NEW.hostname IS NULL OR NEW.hostname = 'plurnk')
1215
1574
  AND (
1216
- NEW.origin != 'plurnk'
1575
+ NEW.origin != '_plurnk'
1217
1576
  OR EXISTS (SELECT 1 FROM workers w WHERE w.id = NEW.worker_id AND w.name = 'plurnk')
1218
1577
  )
1219
1578
  BEGIN
@@ -1247,7 +1606,7 @@ WHEN OLD.state = 'proposed'
1247
1606
  AND (NEW.scheme IS NULL OR NEW.scheme != 'plurnk')
1248
1607
  AND (NEW.scheme IS NULL OR NEW.scheme != 'worker' OR NEW.hostname IS NULL OR NEW.hostname = 'plurnk')
1249
1608
  AND (
1250
- NEW.origin != 'plurnk'
1609
+ NEW.origin != '_plurnk'
1251
1610
  OR EXISTS (SELECT 1 FROM workers w WHERE w.id = NEW.worker_id AND w.name = 'plurnk')
1252
1611
  )
1253
1612
  BEGIN