@plurnk/plurnk-service 1.5.0 → 1.6.1

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 (248) hide show
  1. package/.env.defaults +22 -27
  2. package/INSTALL.md +22 -17
  3. package/README.md +1 -1
  4. package/SPEC.md +505 -362
  5. package/dist/build-info.json +1 -1
  6. package/dist/content/edit-collision.d.ts +5 -0
  7. package/dist/content/edit-collision.d.ts.map +1 -0
  8. package/dist/content/edit-collision.js +14 -0
  9. package/dist/content/edit-collision.js.map +1 -0
  10. package/dist/content/edit-receipt.d.ts +3 -2
  11. package/dist/content/edit-receipt.d.ts.map +1 -1
  12. package/dist/content/edit-receipt.js +58 -13
  13. package/dist/content/edit-receipt.js.map +1 -1
  14. package/dist/content/index.d.ts +4 -1
  15. package/dist/content/index.d.ts.map +1 -1
  16. package/dist/content/index.js +3 -1
  17. package/dist/content/index.js.map +1 -1
  18. package/dist/content/line-anchors.d.ts +41 -0
  19. package/dist/content/line-anchors.d.ts.map +1 -0
  20. package/dist/content/line-anchors.js +191 -0
  21. package/dist/content/line-anchors.js.map +1 -0
  22. package/dist/content/read-projector.d.ts +7 -1
  23. package/dist/content/read-projector.d.ts.map +1 -1
  24. package/dist/content/read-projector.js +46 -10
  25. package/dist/content/read-projector.js.map +1 -1
  26. package/dist/content/read-resolve.js +3 -3
  27. package/dist/content/read-resolve.js.map +1 -1
  28. package/dist/core/BudgetReadout.d.ts.map +1 -1
  29. package/dist/core/BudgetReadout.js +9 -2
  30. package/dist/core/BudgetReadout.js.map +1 -1
  31. package/dist/core/ChannelWrite.js +1 -1
  32. package/dist/core/ChannelWrite.js.map +1 -1
  33. package/dist/core/ChannelWrite.sql +3 -3
  34. package/dist/core/CoreSchemeServices.d.ts +1 -1
  35. package/dist/core/CoreSchemeServices.d.ts.map +1 -1
  36. package/dist/core/CoreSchemeServices.js +1 -1
  37. package/dist/core/CoreSchemeServices.js.map +1 -1
  38. package/dist/core/Dispatcher.d.ts +15 -5
  39. package/dist/core/Dispatcher.d.ts.map +1 -1
  40. package/dist/core/Dispatcher.js +220 -978
  41. package/dist/core/Dispatcher.js.map +1 -1
  42. package/dist/core/DurableStatement.d.ts.map +1 -1
  43. package/dist/core/DurableStatement.js +2 -0
  44. package/dist/core/DurableStatement.js.map +1 -1
  45. package/dist/core/Engine.d.ts +18 -43
  46. package/dist/core/Engine.d.ts.map +1 -1
  47. package/dist/core/Engine.js +90 -1554
  48. package/dist/core/Engine.js.map +1 -1
  49. package/dist/core/Engine.sql +192 -112
  50. package/dist/core/ExecutableTools.d.ts +10 -0
  51. package/dist/core/ExecutableTools.d.ts.map +1 -0
  52. package/dist/core/ExecutableTools.js +41 -0
  53. package/dist/core/ExecutableTools.js.map +1 -0
  54. package/dist/core/ExecutorRegistry.d.ts +3 -3
  55. package/dist/core/ExecutorRegistry.d.ts.map +1 -1
  56. package/dist/core/ExecutorRegistry.js +2 -2
  57. package/dist/core/ExecutorRegistry.js.map +1 -1
  58. package/dist/core/LogBody.d.ts +3 -0
  59. package/dist/core/LogBody.d.ts.map +1 -1
  60. package/dist/core/LogBody.js +21 -5
  61. package/dist/core/LogBody.js.map +1 -1
  62. package/dist/core/LogEntryProjection.d.ts +14 -0
  63. package/dist/core/LogEntryProjection.d.ts.map +1 -0
  64. package/dist/core/LogEntryProjection.js +38 -0
  65. package/dist/core/LogEntryProjection.js.map +1 -0
  66. package/dist/core/ModelCall.d.ts +27 -0
  67. package/dist/core/ModelCall.d.ts.map +1 -0
  68. package/dist/core/ModelCall.js +127 -0
  69. package/dist/core/ModelCall.js.map +1 -0
  70. package/dist/core/PacketBuilder.d.ts +14 -25
  71. package/dist/core/PacketBuilder.d.ts.map +1 -1
  72. package/dist/core/PacketBuilder.js +170 -244
  73. package/dist/core/PacketBuilder.js.map +1 -1
  74. package/dist/core/ProblemLog.d.ts +1 -1
  75. package/dist/core/ProblemLog.d.ts.map +1 -1
  76. package/dist/core/ProblemLog.js +15 -3
  77. package/dist/core/ProblemLog.js.map +1 -1
  78. package/dist/core/ProposalLifecycle.d.ts +2 -2
  79. package/dist/core/ProposalLifecycle.d.ts.map +1 -1
  80. package/dist/core/ProposalLifecycle.js +17 -9
  81. package/dist/core/ProposalLifecycle.js.map +1 -1
  82. package/dist/core/ProviderInstantiate.d.ts.map +1 -1
  83. package/dist/core/ProviderInstantiate.js +25 -8
  84. package/dist/core/ProviderInstantiate.js.map +1 -1
  85. package/dist/core/ResourceMutations.d.ts +73 -0
  86. package/dist/core/ResourceMutations.d.ts.map +1 -0
  87. package/dist/core/ResourceMutations.js +1142 -0
  88. package/dist/core/ResourceMutations.js.map +1 -0
  89. package/dist/core/SchemeRegistry.js +10 -10
  90. package/dist/core/SchemeRegistry.js.map +1 -1
  91. package/dist/core/StoredPacket.d.ts +2 -2
  92. package/dist/core/StoredPacket.js +7 -7
  93. package/dist/core/StrikeRail.d.ts +0 -1
  94. package/dist/core/StrikeRail.d.ts.map +1 -1
  95. package/dist/core/StrikeRail.js +6 -4
  96. package/dist/core/StrikeRail.js.map +1 -1
  97. package/dist/core/TurnRunner.d.ts +72 -0
  98. package/dist/core/TurnRunner.d.ts.map +1 -0
  99. package/dist/core/TurnRunner.js +1730 -0
  100. package/dist/core/TurnRunner.js.map +1 -0
  101. package/dist/core/caps/CapsResolve.js +1 -1
  102. package/dist/core/caps/CapsResolve.js.map +1 -1
  103. package/dist/core/caps/DbChannelCaps.js +4 -4
  104. package/dist/core/caps/DbChannelCaps.js.map +1 -1
  105. package/dist/core/caps/DbEntryCaps.d.ts +2 -1
  106. package/dist/core/caps/DbEntryCaps.d.ts.map +1 -1
  107. package/dist/core/caps/DbEntryCaps.js +8 -5
  108. package/dist/core/caps/DbEntryCaps.js.map +1 -1
  109. package/dist/core/caps/DbProjectionCaps.d.ts +1 -0
  110. package/dist/core/caps/DbProjectionCaps.d.ts.map +1 -1
  111. package/dist/core/caps/DbProjectionCaps.js +29 -0
  112. package/dist/core/caps/DbProjectionCaps.js.map +1 -1
  113. package/dist/core/caps/SchemeCtxImpl.d.ts +5 -2
  114. package/dist/core/caps/SchemeCtxImpl.d.ts.map +1 -1
  115. package/dist/core/caps/SchemeCtxImpl.js +6 -4
  116. package/dist/core/caps/SchemeCtxImpl.js.map +1 -1
  117. package/dist/core/content-hash.js +2 -2
  118. package/dist/core/content-hash.js.map +1 -1
  119. package/dist/core/content-weight.d.ts +2 -0
  120. package/dist/core/content-weight.d.ts.map +1 -0
  121. package/dist/core/content-weight.js +4 -0
  122. package/dist/core/content-weight.js.map +1 -0
  123. package/dist/core/content_weight.d.ts +4 -0
  124. package/dist/core/content_weight.d.ts.map +1 -0
  125. package/dist/core/content_weight.js +6 -0
  126. package/dist/core/content_weight.js.map +1 -0
  127. package/dist/core/fork.d.ts.map +1 -1
  128. package/dist/core/fork.js +23 -7
  129. package/dist/core/fork.js.map +1 -1
  130. package/dist/core/fork.sql +38 -17
  131. package/dist/core/git-membership.d.ts.map +1 -1
  132. package/dist/core/git-membership.js +1 -2
  133. package/dist/core/git-membership.js.map +1 -1
  134. package/dist/core/git-state.js +1 -1
  135. package/dist/core/git-state.js.map +1 -1
  136. package/dist/core/packet-wire.d.ts +6 -3
  137. package/dist/core/packet-wire.d.ts.map +1 -1
  138. package/dist/core/packet-wire.js +257 -99
  139. package/dist/core/packet-wire.js.map +1 -1
  140. package/dist/core/provider-accounting.d.ts +24 -0
  141. package/dist/core/provider-accounting.d.ts.map +1 -0
  142. package/dist/core/provider-accounting.js +76 -0
  143. package/dist/core/provider-accounting.js.map +1 -0
  144. package/dist/core/scheme-types.d.ts +1 -1
  145. package/dist/core/scheme-types.d.ts.map +1 -1
  146. package/dist/core/search-gate.d.ts +2 -2
  147. package/dist/core/search-gate.d.ts.map +1 -1
  148. package/dist/core/search-gate.js +8 -8
  149. package/dist/core/search-gate.js.map +1 -1
  150. package/dist/core/turn-scheduler.js +1 -1
  151. package/dist/core/turn-scheduler.js.map +1 -1
  152. package/dist/digest/Digest.d.ts.map +1 -1
  153. package/dist/digest/Digest.js +221 -110
  154. package/dist/digest/Digest.js.map +1 -1
  155. package/dist/digest/digest.sql +40 -18
  156. package/dist/schemes/Exec.d.ts.map +1 -1
  157. package/dist/schemes/Exec.js +136 -110
  158. package/dist/schemes/Exec.js.map +1 -1
  159. package/dist/schemes/ExecOutputScheme.js +1 -1
  160. package/dist/schemes/ExecOutputScheme.js.map +1 -1
  161. package/dist/schemes/File.d.ts +5 -4
  162. package/dist/schemes/File.d.ts.map +1 -1
  163. package/dist/schemes/File.js +25 -23
  164. package/dist/schemes/File.js.map +1 -1
  165. package/dist/schemes/Log.d.ts +11 -0
  166. package/dist/schemes/Log.d.ts.map +1 -1
  167. package/dist/schemes/Log.js +172 -107
  168. package/dist/schemes/Log.js.map +1 -1
  169. package/dist/schemes/Log.sql +48 -32
  170. package/dist/schemes/Prompt.d.ts +3 -3
  171. package/dist/schemes/Prompt.d.ts.map +1 -1
  172. package/dist/schemes/Prompt.js +4 -1
  173. package/dist/schemes/Prompt.js.map +1 -1
  174. package/dist/schemes/Skill.d.ts +4 -4
  175. package/dist/schemes/Skill.d.ts.map +1 -1
  176. package/dist/schemes/Skill.js +3 -0
  177. package/dist/schemes/Skill.js.map +1 -1
  178. package/dist/schemes/Worker.d.ts +4 -4
  179. package/dist/schemes/Worker.d.ts.map +1 -1
  180. package/dist/schemes/Worker.js +11 -7
  181. package/dist/schemes/Worker.js.map +1 -1
  182. package/dist/schemes/_entry-crud.d.ts +0 -1
  183. package/dist/schemes/_entry-crud.d.ts.map +1 -1
  184. package/dist/schemes/_entry-crud.js +19 -13
  185. package/dist/schemes/_entry-crud.js.map +1 -1
  186. package/dist/schemes/_entry-crud.sql +4 -16
  187. package/dist/schemes/_entry-find.d.ts +17 -16
  188. package/dist/schemes/_entry-find.d.ts.map +1 -1
  189. package/dist/schemes/_entry-find.js +58 -52
  190. package/dist/schemes/_entry-find.js.map +1 -1
  191. package/dist/schemes/_entry-find.sql +2 -21
  192. package/dist/schemes/_entry-graph.sql +1 -1
  193. package/dist/schemes/_entry-manifest.d.ts +5 -17
  194. package/dist/schemes/_entry-manifest.d.ts.map +1 -1
  195. package/dist/schemes/_entry-manifest.js +32 -28
  196. package/dist/schemes/_entry-manifest.js.map +1 -1
  197. package/dist/schemes/_entry-ops.d.ts +5 -4
  198. package/dist/schemes/_entry-ops.d.ts.map +1 -1
  199. package/dist/schemes/_entry-ops.js +89 -49
  200. package/dist/schemes/_entry-ops.js.map +1 -1
  201. package/dist/schemes/_entry-ops.sql +30 -4
  202. package/dist/schemes/_entry-send.js +4 -4
  203. package/dist/schemes/_entry-send.js.map +1 -1
  204. package/dist/schemes/_search-index.d.ts.map +1 -1
  205. package/dist/schemes/_search-index.js +10 -2
  206. package/dist/schemes/_search-index.js.map +1 -1
  207. package/dist/schemes/exec-abort.js +2 -2
  208. package/dist/schemes/exec-abort.js.map +1 -1
  209. package/dist/server/Daemon.d.ts +7 -42
  210. package/dist/server/Daemon.d.ts.map +1 -1
  211. package/dist/server/Daemon.js +139 -900
  212. package/dist/server/Daemon.js.map +1 -1
  213. package/dist/server/DrainSupervisor.d.ts +117 -0
  214. package/dist/server/DrainSupervisor.d.ts.map +1 -0
  215. package/dist/server/DrainSupervisor.js +802 -0
  216. package/dist/server/DrainSupervisor.js.map +1 -0
  217. package/dist/server/drain.sql +2 -2
  218. package/dist/server/envelope.d.ts +0 -2
  219. package/dist/server/envelope.d.ts.map +1 -1
  220. package/dist/server/envelope.js.map +1 -1
  221. package/dist/server/envelope.sql +2 -2
  222. package/dist/server/lifecycle-recovery.sql +28 -15
  223. package/dist/server/logEntry.d.ts +2 -1
  224. package/dist/server/logEntry.d.ts.map +1 -1
  225. package/dist/server/logEntry.js +14 -2
  226. package/dist/server/logEntry.js.map +1 -1
  227. package/dist/server/logEntry.sql +5 -1
  228. package/dist/server/seam-entry-read.sql +2 -2
  229. package/dist/service.js +1 -1
  230. package/dist/service.js.map +1 -1
  231. package/migrations/001_schema.sql +622 -216
  232. package/package.json +39 -34
  233. package/dist/core/BudgetOverflow.d.ts +0 -20
  234. package/dist/core/BudgetOverflow.d.ts.map +0 -1
  235. package/dist/core/BudgetOverflow.js +0 -49
  236. package/dist/core/BudgetOverflow.js.map +0 -1
  237. package/dist/core/caps/DbTagCaps.d.ts +0 -10
  238. package/dist/core/caps/DbTagCaps.d.ts.map +0 -1
  239. package/dist/core/caps/DbTagCaps.js +0 -46
  240. package/dist/core/caps/DbTagCaps.js.map +0 -1
  241. package/dist/core/ruler_count.d.ts +0 -4
  242. package/dist/core/ruler_count.d.ts.map +0 -1
  243. package/dist/core/ruler_count.js +0 -7
  244. package/dist/core/ruler_count.js.map +0 -1
  245. package/dist/core/token-ruler.d.ts +0 -2
  246. package/dist/core/token-ruler.d.ts.map +0 -1
  247. package/dist/core/token-ruler.js +0 -5
  248. package/dist/core/token-ruler.js.map +0 -1
@@ -11,7 +11,6 @@ CREATE TABLE IF NOT EXISTS workspaces (
11
11
  version INTEGER NOT NULL DEFAULT 0 CHECK (version >= 0),
12
12
  name TEXT NOT NULL UNIQUE CHECK (length(name) > 0),
13
13
  created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
14
- cost_usd REAL DEFAULT 0 CHECK (cost_usd IS NULL OR cost_usd >= 0),
15
14
  project_root TEXT,
16
15
  -- {§operator-config} validated client workspace settings; each field composes
17
16
  -- with operator configuration at its owning use site.
@@ -27,9 +26,12 @@ CREATE TABLE IF NOT EXISTS workers (
27
26
  workspace_id INTEGER NOT NULL,
28
27
  name TEXT NOT NULL CHECK (length(name) > 0),
29
28
  created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
29
+ -- {§worker-provider-identity}: provider affinity must not collide when
30
+ -- independent databases reuse local integer ids.
31
+ provider_identity TEXT NOT NULL DEFAULT (lower(hex(randomblob(16))))
32
+ CHECK (length(provider_identity) = 32 AND provider_identity NOT GLOB '*[^0-9a-f]*'),
30
33
  -- workers fork via parent_worker_id; workspaces carry no parent — {§machine-processes-no-fork-workspace}
31
34
  parent_worker_id INTEGER CHECK (parent_worker_id IS NULL OR parent_worker_id != id),
32
- cost_usd REAL DEFAULT 0 CHECK (cost_usd IS NULL OR cost_usd >= 0),
33
35
  origin TEXT NOT NULL DEFAULT 'client' CHECK (origin IN ('model', 'client', 'plurnk')),
34
36
  -- {§methods-model-worker}: durable identity for the workspace's stable
35
37
  -- default conversation; unrelated to its human-facing, reclaimable name.
@@ -44,6 +46,7 @@ CREATE TABLE IF NOT EXISTS workers (
44
46
 
45
47
  CREATE INDEX IF NOT EXISTS workers_workspace_id_created_at ON workers (workspace_id, created_at);
46
48
  CREATE INDEX IF NOT EXISTS workers_parent_worker_id ON workers (parent_worker_id);
49
+ CREATE UNIQUE INDEX IF NOT EXISTS workers_provider_identity ON workers (provider_identity);
47
50
  CREATE UNIQUE INDEX IF NOT EXISTS workers_workspace_default_conversation
48
51
  ON workers (workspace_id) WHERE default_conversation = 1;
49
52
  -- NOT unique: a name is frozen per worker ({§machine-processes-worker-origin}) but RECLAIMABLE across
@@ -52,6 +55,13 @@ CREATE UNIQUE INDEX IF NOT EXISTS workers_workspace_default_conversation
52
55
  -- → worker_live_by_name → 409), never by this index. Indexed for the by-name resolve/spawn lookup.
53
56
  CREATE INDEX IF NOT EXISTS workers_workspace_name ON workers (workspace_id, name);
54
57
 
58
+ CREATE TRIGGER IF NOT EXISTS workers_provider_identity_immutable
59
+ BEFORE UPDATE OF provider_identity ON workers
60
+ WHEN NEW.provider_identity != OLD.provider_identity
61
+ BEGIN
62
+ SELECT RAISE(ABORT, 'workers.provider_identity is immutable');
63
+ END;
64
+
55
65
  -- {§env-delta-log-pull}: one append-only occurrence journal gives every ambient
56
66
  -- producer a shared monotonic order. It snapshots only what the observer row
57
67
  -- needs; shared-world contents remain owned by entries/files, never copied here.
@@ -70,6 +80,7 @@ CREATE TABLE IF NOT EXISTS ambient_events (
70
80
  pathname TEXT,
71
81
  rx TEXT,
72
82
  attrs TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(attrs)),
83
+ tags TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(tags) AND json_type(tags) = 'array'),
73
84
  status_rx INTEGER NOT NULL CHECK (status_rx BETWEEN 100 AND 599),
74
85
  terminated_by TEXT CHECK (terminated_by IS NULL OR terminated_by = 'cancel'),
75
86
  created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
@@ -198,9 +209,9 @@ BEGIN
198
209
  END;
199
210
 
200
211
  -- turns
201
- -- finish_reason / model: provider-call metadata from the provider response contract.
202
- -- Properties of the call, not of the model's emission payload — kept on
203
- -- the Turn row alongside usage rather than nested into packet.assistant.
212
+ -- finish_reason / model: accepted provider-call metadata from the provider
213
+ -- response contract. Physical request accounting is normalized beneath the
214
+ -- logical emission attempt that caused it.
204
215
  CREATE TABLE IF NOT EXISTS turns (
205
216
  id INTEGER NOT NULL PRIMARY KEY,
206
217
  version INTEGER NOT NULL DEFAULT 0 CHECK (version >= 0),
@@ -208,15 +219,9 @@ CREATE TABLE IF NOT EXISTS turns (
208
219
  sequence INTEGER NOT NULL CHECK (sequence >= 1),
209
220
  timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
210
221
  status INTEGER NOT NULL CHECK (status BETWEEN 100 AND 599),
211
- usage_prompt INTEGER NOT NULL DEFAULT 0 CHECK (usage_prompt >= 0),
212
- usage_completion INTEGER NOT NULL DEFAULT 0 CHECK (usage_completion >= 0),
213
- usage_reasoning INTEGER NOT NULL DEFAULT 0 CHECK (usage_reasoning >= 0),
214
- usage_cached INTEGER NOT NULL DEFAULT 0 CHECK (usage_cached >= 0),
215
- usage_cost TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(usage_cost) AND json_type(usage_cost) = 'array'),
216
- usage_cost_usd REAL DEFAULT 0 CHECK (usage_cost_usd IS NULL OR usage_cost_usd >= 0),
217
- -- Effective packet allowance for this turn's provider and policy; NULL is
218
- -- uncapped or unknown. {§tokenomics-client-gauge}
219
- usage_prompt_budget INTEGER CHECK (usage_prompt_budget IS NULL OR usage_prompt_budget >= 1),
222
+ -- Provider-derived curation calibration for this turn; NULL when input
223
+ -- capacity is unknown. {§tokenomics-client-gauge}
224
+ usage_curation_budget INTEGER CHECK (usage_curation_budget IS NULL OR usage_curation_budget >= 1),
220
225
  -- {§packet-stored-shape}: NULL means no model request was assembled. A
221
226
  -- present packet is either the measured request or that request extended
222
227
  -- by the paired admitted-response fields.
@@ -226,8 +231,8 @@ CREATE TABLE IF NOT EXISTS turns (
226
231
  WHEN json_valid(packet) = 0 THEN 0
227
232
  ELSE COALESCE(
228
233
  json_type(packet) = 'object'
229
- AND json_type(packet, '$.tokens') = 'integer'
230
- AND json_extract(packet, '$.tokens') >= 0
234
+ AND json_type(packet, '$.weight') = 'integer'
235
+ AND json_extract(packet, '$.weight') >= 0
231
236
  AND json_type(packet, '$.sections') = 'array'
232
237
  AND json_type(packet, '$.attributions') = 'array'
233
238
  AND (
@@ -258,148 +263,261 @@ CREATE TABLE IF NOT EXISTS turns (
258
263
  CREATE UNIQUE INDEX IF NOT EXISTS turns_loop_id_sequence ON turns (loop_id, sequence);
259
264
  CREATE INDEX IF NOT EXISTS turns_timestamp ON turns (timestamp);
260
265
 
261
- -- Provider calls are attempts beneath one engine turn. Rejected and interrupted
262
- -- response evidence persists without becoming a turn or entering model-visible
263
- -- history.
264
- CREATE TABLE IF NOT EXISTS turn_attempts (
266
+ -- One logical provider.generate call. Emission attempts and BARE inferences
267
+ -- share response/failure evidence and cardinal physical request accounting;
268
+ -- operation-specific semantics live in their specializing relations.
269
+ CREATE TABLE IF NOT EXISTS model_calls (
265
270
  id INTEGER NOT NULL PRIMARY KEY,
266
271
  turn_id INTEGER NOT NULL,
267
272
  sequence INTEGER NOT NULL CHECK (sequence >= 1),
273
+ kind TEXT NOT NULL CHECK (kind IN ('emission', 'bare')),
268
274
  state TEXT NOT NULL DEFAULT 'pending' CHECK (state IN ('pending', 'response', 'error')),
269
- accepted INTEGER CHECK (accepted IS NULL OR accepted IN (0, 1)),
270
275
  response TEXT CHECK (response IS NULL OR json_valid(response)),
271
276
  failure TEXT CHECK (failure IS NULL OR json_valid(failure)),
272
- parse_errors TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(parse_errors)),
277
+ -- Request-shaped provider capacity evidence. A completed response always
278
+ -- has it; a pre-I/O or transport failure may retain it without fabricating
279
+ -- physical usage.
280
+ capacity TEXT CHECK (
281
+ capacity IS NULL OR (json_valid(capacity) AND json_type(capacity) = 'object')
282
+ ),
273
283
  -- Exact opaque tag set forwarded with this provider call.
274
284
  -- {§attribution}
275
285
  attributions TEXT NOT NULL DEFAULT '[]' CHECK (
276
286
  json_valid(attributions) AND json_type(attributions) = 'array'
277
287
  ),
278
- usage_prompt INTEGER CHECK (usage_prompt IS NULL OR usage_prompt >= 0),
279
- usage_completion INTEGER CHECK (usage_completion IS NULL OR usage_completion >= 0),
280
- usage_reasoning INTEGER CHECK (usage_reasoning IS NULL OR usage_reasoning >= 0),
281
- usage_cached INTEGER CHECK (usage_cached IS NULL OR usage_cached >= 0),
282
- usage_cost TEXT CHECK (usage_cost IS NULL OR (json_valid(usage_cost) AND json_type(usage_cost) = 'object')),
283
- usage_cost_usd REAL CHECK (usage_cost_usd IS NULL OR usage_cost_usd >= 0),
284
288
  finish_reason TEXT,
285
289
  model TEXT NOT NULL CHECK (length(model) >= 1),
286
290
  timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
287
291
  completed_at TEXT,
288
292
  CHECK (
289
293
  (state = 'pending'
290
- AND accepted IS NULL AND response IS NULL AND failure IS NULL
291
- AND usage_prompt IS NULL AND usage_completion IS NULL
292
- AND usage_reasoning IS NULL AND usage_cached IS NULL
293
- AND usage_cost IS NULL AND usage_cost_usd IS NULL
294
+ AND response IS NULL AND failure IS NULL
295
+ AND capacity IS NULL
294
296
  AND finish_reason IS NULL AND completed_at IS NULL)
295
297
  OR
296
298
  (state = 'response'
297
299
  AND response IS NOT NULL
298
- AND usage_prompt IS NOT NULL AND usage_completion IS NOT NULL
299
- AND usage_reasoning IS NOT NULL AND usage_cached IS NOT NULL
300
- AND usage_cost IS NOT NULL AND completed_at IS NOT NULL)
300
+ AND capacity IS NOT NULL
301
+ AND completed_at IS NOT NULL)
301
302
  OR
302
303
  (state = 'error'
303
- AND accepted IS NULL AND response IS NULL AND failure IS NOT NULL
304
- AND usage_prompt = 0 AND usage_completion = 0
305
- AND usage_reasoning = 0 AND usage_cached = 0
306
- AND usage_cost IS NOT NULL AND completed_at IS NOT NULL)
304
+ AND response IS NULL AND failure IS NOT NULL
305
+ AND completed_at IS NOT NULL)
307
306
  ),
308
307
  UNIQUE (turn_id, sequence),
309
308
  FOREIGN KEY (turn_id) REFERENCES turns(id) ON DELETE CASCADE
310
309
  ) STRICT;
311
310
 
312
- CREATE INDEX IF NOT EXISTS turn_attempts_turn_id ON turn_attempts (turn_id);
313
- CREATE UNIQUE INDEX IF NOT EXISTS turn_attempts_one_accepted_per_turn
314
- ON turn_attempts (turn_id)
315
- WHERE accepted = 1;
311
+ CREATE INDEX IF NOT EXISTS model_calls_turn_id ON model_calls (turn_id, sequence);
316
312
 
317
- CREATE TRIGGER IF NOT EXISTS turn_attempts_request_identity_immutable
318
- BEFORE UPDATE OF turn_id, sequence, attributions, timestamp ON turn_attempts
313
+ CREATE TRIGGER IF NOT EXISTS model_calls_request_identity_immutable
314
+ BEFORE UPDATE OF turn_id, sequence, kind, attributions, timestamp ON model_calls
319
315
  BEGIN
320
- SELECT RAISE(ABORT, 'provider attempt request identity is immutable');
316
+ SELECT RAISE(ABORT, 'model call request identity is immutable');
321
317
  END;
322
318
 
323
- CREATE TRIGGER IF NOT EXISTS turn_attempts_state_forward_only
324
- BEFORE UPDATE OF state ON turn_attempts
319
+ CREATE TRIGGER IF NOT EXISTS model_calls_state_forward_only
320
+ BEFORE UPDATE OF state ON model_calls
325
321
  WHEN NOT (
326
322
  NEW.state = OLD.state
327
323
  OR (OLD.state = 'pending' AND NEW.state IN ('response', 'error'))
328
324
  )
329
325
  BEGIN
330
- SELECT RAISE(ABORT, 'provider attempt state may only close once');
326
+ SELECT RAISE(ABORT, 'model call state may only close once');
331
327
  END;
332
328
 
333
- CREATE TRIGGER IF NOT EXISTS turn_attempts_observation_immutable
334
- BEFORE UPDATE OF response, usage_prompt, usage_completion, usage_reasoning,
335
- usage_cached, finish_reason, model, completed_at
336
- ON turn_attempts
329
+ CREATE TRIGGER IF NOT EXISTS model_calls_observation_immutable
330
+ BEFORE UPDATE OF response, failure, capacity, finish_reason, model, completed_at
331
+ ON model_calls
337
332
  WHEN OLD.state != 'pending'
338
333
  BEGIN
339
- SELECT RAISE(ABORT, 'provider attempt observation is immutable');
334
+ SELECT RAISE(ABORT, 'model call observation is immutable');
335
+ END;
336
+
337
+ -- Emission admission specializes one model call without re-owning its response,
338
+ -- provider identity, ordering, or accounting evidence.
339
+ CREATE TABLE IF NOT EXISTS turn_attempts (
340
+ id INTEGER NOT NULL PRIMARY KEY,
341
+ model_call_id INTEGER NOT NULL UNIQUE,
342
+ accepted INTEGER CHECK (accepted IS NULL OR accepted IN (0, 1)),
343
+ parse_errors TEXT NOT NULL DEFAULT '[]' CHECK (
344
+ json_valid(parse_errors) AND json_type(parse_errors) = 'array'
345
+ ),
346
+ FOREIGN KEY (model_call_id) REFERENCES model_calls(id) ON DELETE CASCADE
347
+ ) STRICT;
348
+
349
+ CREATE TRIGGER IF NOT EXISTS turn_attempts_request_identity_immutable
350
+ BEFORE UPDATE OF model_call_id ON turn_attempts
351
+ BEGIN
352
+ SELECT RAISE(ABORT, 'emission attempt identity is immutable');
353
+ END;
354
+
355
+ CREATE TRIGGER IF NOT EXISTS turn_attempts_emission_call_only
356
+ BEFORE INSERT ON turn_attempts
357
+ WHEN COALESCE((SELECT kind = 'emission' FROM model_calls WHERE id = NEW.model_call_id), 0) != 1
358
+ BEGIN
359
+ SELECT RAISE(ABORT, 'turn attempt requires an emission model call');
360
+ END;
361
+
362
+ CREATE TRIGGER IF NOT EXISTS turn_attempts_classification_after_response
363
+ BEFORE UPDATE OF accepted, parse_errors ON turn_attempts
364
+ WHEN COALESCE((SELECT state = 'response' FROM model_calls WHERE id = OLD.model_call_id), 0) != 1
365
+ BEGIN
366
+ SELECT RAISE(ABORT, 'emission classification requires model response evidence');
340
367
  END;
341
368
 
342
369
  CREATE TRIGGER IF NOT EXISTS turn_attempts_classification_once
343
- BEFORE UPDATE OF accepted, parse_errors, failure, usage_cost, usage_cost_usd
370
+ BEFORE UPDATE OF accepted, parse_errors
344
371
  ON turn_attempts
345
- WHEN OLD.state = 'error' OR (OLD.state = 'response' AND OLD.accepted IS NOT NULL)
372
+ WHEN OLD.accepted IS NOT NULL
346
373
  BEGIN
347
- SELECT RAISE(ABORT, 'provider attempt classification is immutable');
374
+ SELECT RAISE(ABORT, 'emission attempt classification is immutable');
348
375
  END;
349
376
 
350
- -- Attempt rows are the monetary and provider-usage authority. Turns retain a
351
- -- denormalized projection for their public read contract, maintained here so a
352
- -- crash between provider I/O and engine closure cannot leave a false zero.
353
- CREATE VIEW IF NOT EXISTS turn_attempt_rollups AS
354
- SELECT t.id AS turn_id,
355
- COALESCE(SUM(a.usage_prompt), 0) AS usage_prompt,
356
- COALESCE(SUM(a.usage_completion), 0) AS usage_completion,
357
- COALESCE(SUM(a.usage_reasoning), 0) AS usage_reasoning,
358
- COALESCE(SUM(a.usage_cached), 0) AS usage_cached,
359
- COALESCE((
360
- SELECT json_group_array(json(ordered.usage_cost))
361
- FROM (
362
- SELECT observed.usage_cost
363
- FROM turn_attempts observed
364
- WHERE observed.turn_id = t.id AND observed.usage_cost IS NOT NULL
365
- ORDER BY observed.sequence
366
- ) ordered
367
- ), '[]') AS usage_cost,
368
- CASE
369
- WHEN COUNT(a.id) = 0 THEN 0
370
- WHEN COUNT(a.id) FILTER (WHERE a.usage_cost_usd IS NULL) > 0 THEN NULL
371
- ELSE COALESCE(SUM(a.usage_cost_usd), 0)
372
- END AS usage_cost_usd
373
- FROM turns t
374
- LEFT JOIN turn_attempts a ON a.turn_id = t.id
375
- GROUP BY t.id;
376
-
377
- CREATE TRIGGER IF NOT EXISTS turn_attempts_rollup_insert
378
- AFTER INSERT ON turn_attempts
377
+ CREATE TRIGGER IF NOT EXISTS turn_attempts_one_accepted_insert
378
+ BEFORE INSERT ON turn_attempts
379
+ WHEN NEW.accepted = 1 AND EXISTS (
380
+ SELECT 1
381
+ FROM turn_attempts existing
382
+ JOIN model_calls old_call ON old_call.id = existing.model_call_id
383
+ JOIN model_calls new_call ON new_call.id = NEW.model_call_id
384
+ WHERE old_call.turn_id = new_call.turn_id AND existing.accepted = 1
385
+ )
379
386
  BEGIN
380
- UPDATE turns SET
381
- usage_prompt = (SELECT usage_prompt FROM turn_attempt_rollups WHERE turn_id = NEW.turn_id),
382
- usage_completion = (SELECT usage_completion FROM turn_attempt_rollups WHERE turn_id = NEW.turn_id),
383
- usage_reasoning = (SELECT usage_reasoning FROM turn_attempt_rollups WHERE turn_id = NEW.turn_id),
384
- usage_cached = (SELECT usage_cached FROM turn_attempt_rollups WHERE turn_id = NEW.turn_id),
385
- usage_cost = (SELECT usage_cost FROM turn_attempt_rollups WHERE turn_id = NEW.turn_id),
386
- usage_cost_usd = (SELECT usage_cost_usd FROM turn_attempt_rollups WHERE turn_id = NEW.turn_id)
387
- WHERE id = NEW.turn_id;
387
+ SELECT RAISE(ABORT, 'one emission may be accepted per turn');
388
388
  END;
389
389
 
390
- CREATE TRIGGER IF NOT EXISTS turn_attempts_rollup_update
391
- AFTER UPDATE OF state, usage_prompt, usage_completion, usage_reasoning,
392
- usage_cached, usage_cost, usage_cost_usd
393
- ON turn_attempts
390
+ CREATE TRIGGER IF NOT EXISTS turn_attempts_one_accepted_update
391
+ BEFORE UPDATE OF accepted ON turn_attempts
392
+ WHEN NEW.accepted = 1 AND EXISTS (
393
+ SELECT 1
394
+ FROM turn_attempts existing
395
+ JOIN model_calls old_call ON old_call.id = existing.model_call_id
396
+ JOIN model_calls new_call ON new_call.id = NEW.model_call_id
397
+ WHERE old_call.turn_id = new_call.turn_id
398
+ AND existing.id != OLD.id
399
+ AND existing.accepted = 1
400
+ )
401
+ BEGIN
402
+ SELECT RAISE(ABORT, 'one emission may be accepted per turn');
403
+ END;
404
+
405
+ -- {§provider-request-accounting}: one row is opened before one physical
406
+ -- provider request and settled exactly once with the evidence from that request.
407
+ -- Monetary values remain canonical decimal strings; SQLite REAL is never an
408
+ -- accounting representation.
409
+ CREATE TABLE IF NOT EXISTS provider_requests (
410
+ id INTEGER NOT NULL PRIMARY KEY,
411
+ model_call_id INTEGER NOT NULL,
412
+ sequence INTEGER NOT NULL CHECK (sequence >= 1),
413
+ provider TEXT NOT NULL CHECK (length(provider) > 0),
414
+ model TEXT NOT NULL CHECK (length(model) > 0),
415
+ state TEXT NOT NULL DEFAULT 'pending' CHECK (state IN ('pending', 'settled')),
416
+ outcome TEXT CHECK (outcome IN ('response', 'error')),
417
+ status INTEGER CHECK (status IS NULL OR status BETWEEN 100 AND 599),
418
+ usage_input INTEGER CHECK (usage_input IS NULL OR usage_input >= 0),
419
+ usage_output INTEGER CHECK (usage_output IS NULL OR usage_output >= 0),
420
+ usage_total INTEGER CHECK (usage_total IS NULL OR usage_total >= 0),
421
+ usage_input_no_cache INTEGER CHECK (usage_input_no_cache IS NULL OR usage_input_no_cache >= 0),
422
+ usage_input_cache_read INTEGER CHECK (usage_input_cache_read IS NULL OR usage_input_cache_read >= 0),
423
+ usage_input_cache_write INTEGER CHECK (usage_input_cache_write IS NULL OR usage_input_cache_write >= 0),
424
+ usage_output_text INTEGER CHECK (usage_output_text IS NULL OR usage_output_text >= 0),
425
+ usage_output_reasoning INTEGER CHECK (usage_output_reasoning IS NULL OR usage_output_reasoning >= 0),
426
+ cost_kind TEXT CHECK (cost_kind IN ('charged', 'estimated', 'unknown')),
427
+ cost_amount TEXT,
428
+ cost_currency TEXT,
429
+ cost_usd_equivalent TEXT,
430
+ cost_source TEXT,
431
+ cost_reason TEXT,
432
+ started_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
433
+ completed_at TEXT,
434
+ CHECK (
435
+ (state = 'pending'
436
+ AND outcome IS NULL AND status IS NULL
437
+ AND usage_input IS NULL AND usage_output IS NULL AND usage_total IS NULL
438
+ AND usage_input_no_cache IS NULL AND usage_input_cache_read IS NULL
439
+ AND usage_input_cache_write IS NULL AND usage_output_text IS NULL
440
+ AND usage_output_reasoning IS NULL
441
+ AND cost_kind IS NULL AND cost_amount IS NULL AND cost_currency IS NULL
442
+ AND cost_usd_equivalent IS NULL AND cost_source IS NULL AND cost_reason IS NULL
443
+ AND completed_at IS NULL)
444
+ OR
445
+ (state = 'settled' AND outcome IS NOT NULL AND cost_kind IS NOT NULL AND completed_at IS NOT NULL)
446
+ ),
447
+ CHECK (
448
+ cost_amount IS NULL OR (
449
+ length(cost_amount) > 0
450
+ AND cost_amount NOT GLOB '*[^0-9.]*'
451
+ AND cost_amount NOT LIKE '.%'
452
+ AND cost_amount NOT LIKE '%.'
453
+ AND cost_amount NOT LIKE '%.%.%'
454
+ AND (cost_amount = '0' OR cost_amount NOT GLOB '0[0-9]*')
455
+ )
456
+ ),
457
+ CHECK (
458
+ cost_usd_equivalent IS NULL OR (
459
+ length(cost_usd_equivalent) > 0
460
+ AND cost_usd_equivalent NOT GLOB '*[^0-9.]*'
461
+ AND cost_usd_equivalent NOT LIKE '.%'
462
+ AND cost_usd_equivalent NOT LIKE '%.'
463
+ AND cost_usd_equivalent NOT LIKE '%.%.%'
464
+ AND (cost_usd_equivalent = '0' OR cost_usd_equivalent NOT GLOB '0[0-9]*')
465
+ )
466
+ ),
467
+ CHECK (
468
+ cost_currency IS NULL OR (
469
+ length(cost_currency) BETWEEN 3 AND 12
470
+ AND substr(cost_currency, 1, 1) GLOB '[A-Z]'
471
+ AND cost_currency NOT GLOB '*[^A-Z0-9]*'
472
+ )
473
+ ),
474
+ CHECK (
475
+ cost_kind IS NULL
476
+ OR (cost_kind = 'charged'
477
+ AND cost_amount IS NOT NULL AND cost_currency IS NOT NULL
478
+ AND cost_source IS NOT NULL AND length(cost_source) > 0
479
+ AND cost_reason IS NULL)
480
+ OR (cost_kind = 'estimated'
481
+ AND cost_amount IS NOT NULL AND cost_currency IS NOT NULL
482
+ AND cost_usd_equivalent IS NULL
483
+ AND cost_source IS NOT NULL AND length(cost_source) > 0
484
+ AND cost_reason IS NULL)
485
+ OR (cost_kind = 'unknown'
486
+ AND cost_amount IS NULL AND cost_currency IS NULL
487
+ AND cost_usd_equivalent IS NULL AND cost_source IS NULL
488
+ AND cost_reason IS NOT NULL AND length(cost_reason) > 0)
489
+ ),
490
+ UNIQUE (model_call_id, sequence),
491
+ FOREIGN KEY (model_call_id) REFERENCES model_calls(id) ON DELETE CASCADE
492
+ ) STRICT;
493
+
494
+ CREATE INDEX IF NOT EXISTS provider_requests_model_call_id
495
+ ON provider_requests (model_call_id, sequence);
496
+
497
+ CREATE TRIGGER IF NOT EXISTS provider_requests_identity_immutable
498
+ BEFORE UPDATE OF model_call_id, sequence, provider, model, started_at ON provider_requests
499
+ BEGIN
500
+ SELECT RAISE(ABORT, 'provider request identity is immutable');
501
+ END;
502
+
503
+ CREATE TRIGGER IF NOT EXISTS provider_requests_state_forward_only
504
+ BEFORE UPDATE OF state ON provider_requests
505
+ WHEN NOT (NEW.state = OLD.state OR (OLD.state = 'pending' AND NEW.state = 'settled'))
394
506
  BEGIN
395
- UPDATE turns SET
396
- usage_prompt = (SELECT usage_prompt FROM turn_attempt_rollups WHERE turn_id = NEW.turn_id),
397
- usage_completion = (SELECT usage_completion FROM turn_attempt_rollups WHERE turn_id = NEW.turn_id),
398
- usage_reasoning = (SELECT usage_reasoning FROM turn_attempt_rollups WHERE turn_id = NEW.turn_id),
399
- usage_cached = (SELECT usage_cached FROM turn_attempt_rollups WHERE turn_id = NEW.turn_id),
400
- usage_cost = (SELECT usage_cost FROM turn_attempt_rollups WHERE turn_id = NEW.turn_id),
401
- usage_cost_usd = (SELECT usage_cost_usd FROM turn_attempt_rollups WHERE turn_id = NEW.turn_id)
402
- WHERE id = NEW.turn_id;
507
+ SELECT RAISE(ABORT, 'provider request may only settle once');
508
+ END;
509
+
510
+ CREATE TRIGGER IF NOT EXISTS provider_requests_settlement_immutable
511
+ BEFORE UPDATE OF outcome, status,
512
+ usage_input, usage_output, usage_total,
513
+ usage_input_no_cache, usage_input_cache_read, usage_input_cache_write,
514
+ usage_output_text, usage_output_reasoning,
515
+ cost_kind, cost_amount, cost_currency, cost_usd_equivalent,
516
+ cost_source, cost_reason, completed_at
517
+ ON provider_requests
518
+ WHEN OLD.state != 'pending'
519
+ BEGIN
520
+ SELECT RAISE(ABORT, 'provider request settlement is immutable');
403
521
  END;
404
522
 
405
523
  -- derivations
@@ -413,6 +531,7 @@ CREATE TABLE IF NOT EXISTS derivations (
413
531
  state TEXT NOT NULL DEFAULT 'building' CHECK (state IN ('building', 'complete')),
414
532
  disposition TEXT CHECK (disposition IN ('vector', 'lexical', 'excluded', 'nonsemantic', 'failed')),
415
533
  reason TEXT,
534
+ parse_issues INTEGER CHECK (parse_issues IS NULL OR parse_issues > 0),
416
535
  CHECK ((state = 'building' AND disposition IS NULL) OR (state = 'complete' AND disposition IS NOT NULL))
417
536
  ) STRICT;
418
537
 
@@ -476,9 +595,9 @@ CREATE TABLE IF NOT EXISTS entry_channels (
476
595
  name TEXT NOT NULL CHECK (length(name) > 0),
477
596
  content TEXT NOT NULL CHECK (length(content) <= 104857600),
478
597
  mimetype TEXT NOT NULL CHECK (length(mimetype) > 0),
479
- tokens INTEGER NOT NULL DEFAULT 0 CHECK (tokens >= 0),
480
- -- content identity: sha256 of content, stamped at static writes (streamed appends leave it
481
- -- NULL). The per-tokenizer token cache it once keyed was retired — {§tokenomics-agnostic-ruler}.
598
+ weight INTEGER NOT NULL DEFAULT 0 CHECK (weight >= 0),
599
+ -- Content identity: sha256 of content, stamped at static writes; streamed
600
+ -- appends leave it NULL. Curation weight remains model-independent.
482
601
  content_hash TEXT,
483
602
  state TEXT NOT NULL DEFAULT 'static' CHECK (state IN ('static', 'active', 'closed', 'errored')),
484
603
  -- Exact terminal producer evidence for this representation channel. NULL is
@@ -524,14 +643,11 @@ BEGIN
524
643
  UPDATE entries SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = NEW.entry_id;
525
644
  END;
526
645
 
527
- CREATE TABLE IF NOT EXISTS entry_tags (
528
- entry_id INTEGER NOT NULL,
529
- tag TEXT NOT NULL CHECK (length(tag) > 0),
530
- PRIMARY KEY (entry_id, tag),
531
- FOREIGN KEY (entry_id) REFERENCES entries(id) ON DELETE CASCADE
532
- ) STRICT, WITHOUT ROWID;
533
-
534
- CREATE INDEX IF NOT EXISTS entry_tags_tag ON entry_tags (tag);
646
+ CREATE TRIGGER IF NOT EXISTS entries_touch_on_channel_update
647
+ AFTER UPDATE ON entry_channels
648
+ BEGIN
649
+ UPDATE entries SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = NEW.entry_id;
650
+ END;
535
651
 
536
652
  -- symbol_defs
537
653
  -- @graph NODES ({§relation-indexed-dialects}). Code symbol definitions, populated
@@ -623,12 +739,15 @@ CREATE TABLE IF NOT EXISTS log_entries (
623
739
  ambient_event_id INTEGER REFERENCES ambient_events(id),
624
740
  -- Search derivation attached to this durable log result, when available.
625
741
  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.
744
+ model_call_id INTEGER REFERENCES model_calls(id),
626
745
 
627
746
  -- 'error' is an ACTIONLESS row ({§operation-results} — errors are log items): a parse failure that
628
747
  -- produced no op still records a log entry (op='error', status_rx≥400, no target) so the model
629
748
  -- can fold/kill/recall its own mistakes like any other log row — one budget surface, the log.
630
- -- A model-emission artifact ({§model-entry}) carries NULL here and
631
- -- attrs.kind='model_emission': no operation was executed or fabricated.
749
+ -- Actionless artifacts carry NULL here: kernel-authored worker initialization
750
+ -- ({§worker-initialization-entry}) or a model emission ({§model-entry}).
632
751
  -- No op enum here: the grammar op set is grammar's contract (PlurnkOp), and this column is written
633
752
  -- only by the PlurnkOp-typed engine (grammar ops), service row selectors, or NULL for no op.
634
753
  -- A SQL enum would be a hand-copy of grammar's op list that silently goes stale on every new verb
@@ -656,7 +775,9 @@ CREATE TABLE IF NOT EXISTS log_entries (
656
775
  mimetype_rx TEXT NOT NULL CHECK (length(mimetype_rx) > 0),
657
776
  status_rx INTEGER NOT NULL CHECK (status_rx BETWEEN 100 AND 599),
658
777
 
659
- tokens INTEGER NOT NULL DEFAULT 0 CHECK (tokens >= 0),
778
+ -- Complete canonical LogBody content before coordinate/presentation
779
+ -- projection; persistence envelopes do not contribute. {§tokenomics-weight-stored-at-write}
780
+ weight INTEGER NOT NULL DEFAULT 0 CHECK (weight >= 0),
660
781
 
661
782
  state TEXT NOT NULL DEFAULT 'resolved'
662
783
  CHECK (state IN ('proposed', 'resolved', 'failed', 'cancelled')),
@@ -665,7 +786,9 @@ CREATE TABLE IF NOT EXISTS log_entries (
665
786
 
666
787
  expanded INTEGER NOT NULL DEFAULT 1 CHECK (expanded IN (0, 1)),
667
788
 
668
- CHECK ((op IS NULL) = COALESCE(json_extract(attrs, '$.kind') = 'model_emission', 0)),
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'),
669
792
 
670
793
  FOREIGN KEY (worker_id) REFERENCES workers(id) ON DELETE CASCADE,
671
794
  FOREIGN KEY (loop_id) REFERENCES loops(id) ON DELETE CASCADE,
@@ -676,37 +799,388 @@ CREATE UNIQUE INDEX IF NOT EXISTS log_entries_turn_id_sequence ON log_entries (t
676
799
  CREATE INDEX IF NOT EXISTS log_entries_worker_id ON log_entries (worker_id);
677
800
  CREATE INDEX IF NOT EXISTS log_entries_loop_id ON log_entries (loop_id);
678
801
  CREATE INDEX IF NOT EXISTS log_entries_at ON log_entries (at);
802
+ CREATE UNIQUE INDEX IF NOT EXISTS log_entries_model_call_id
803
+ ON log_entries (model_call_id)
804
+ WHERE model_call_id IS NOT NULL;
679
805
  CREATE UNIQUE INDEX IF NOT EXISTS log_entries_worker_ambient_event
680
806
  ON log_entries (worker_id, ambient_event_id)
681
807
  WHERE ambient_event_id IS NOT NULL;
682
808
 
683
- -- {§log-region-tagging} — named log-region curation. FOLD is the log's write-op (EDIT can't
684
- -- reach engine-written rows): FOLD[tag] stamps a tag on a region; OPEN[tag]/FIND[tag] filter
685
- -- by it. Mirrors entry_tags (apply additive / filter ALL-tags AND); CASCADE with the row on KILL.
809
+ CREATE TRIGGER IF NOT EXISTS log_entries_model_call_valid
810
+ BEFORE INSERT ON log_entries
811
+ WHEN NEW.model_call_id IS NOT NULL
812
+ AND NOT EXISTS (
813
+ SELECT 1
814
+ FROM model_calls call
815
+ WHERE call.id = NEW.model_call_id
816
+ AND call.turn_id = NEW.turn_id
817
+ AND call.state != 'pending'
818
+ AND (
819
+ (call.kind = 'bare' AND NEW.op = 'BARE')
820
+ OR (
821
+ call.kind = 'emission'
822
+ AND NEW.op IS NULL
823
+ AND json_extract(NEW.attrs, '$.kind') = 'model_emission'
824
+ )
825
+ )
826
+ )
827
+ BEGIN
828
+ SELECT RAISE(ABORT, 'log entry model call does not match its represented result');
829
+ END;
830
+
831
+ -- {§log-item-tags} — log classification plus OPEN/FOLD selection and mutation.
832
+ -- CASCADE erases the classification with its row.
686
833
  CREATE TABLE IF NOT EXISTS log_tags (
687
834
  log_entry_id INTEGER NOT NULL,
688
- tag TEXT NOT NULL CHECK (length(tag) > 0),
835
+ tag TEXT NOT NULL,
689
836
  PRIMARY KEY (log_entry_id, tag),
690
837
  FOREIGN KEY (log_entry_id) REFERENCES log_entries(id) ON DELETE CASCADE
691
838
  ) STRICT, WITHOUT ROWID;
692
839
 
840
+ -- The relational table owns stored tag identity. Producers and curation
841
+ -- signals carry signs, but the stored name never does; delimiters, whitespace,
842
+ -- and control characters cannot become durable classifications through a
843
+ -- private SQL caller either.
844
+ CREATE TRIGGER IF NOT EXISTS log_tags_name_valid
845
+ BEFORE INSERT ON log_tags
846
+ WHEN length(NEW.tag) = 0
847
+ OR substr(NEW.tag, 1, 1) IN ('+', '-')
848
+ OR instr(NEW.tag, '[') > 0
849
+ OR instr(NEW.tag, ']') > 0
850
+ OR instr(NEW.tag, ',') > 0
851
+ OR instr(NEW.tag, char(0)) > 0
852
+ OR EXISTS (
853
+ WITH RECURSIVE offsets(position) AS (
854
+ SELECT 1
855
+ UNION ALL
856
+ SELECT position + 1 FROM offsets WHERE position < length(NEW.tag)
857
+ )
858
+ SELECT 1
859
+ FROM offsets
860
+ WHERE unicode(substr(NEW.tag, position, 1)) BETWEEN 0 AND 32
861
+ OR unicode(substr(NEW.tag, position, 1)) BETWEEN 127 AND 159
862
+ OR unicode(substr(NEW.tag, position, 1)) IN (160, 5760, 8232, 8233, 8239, 8287, 12288, 65279)
863
+ OR unicode(substr(NEW.tag, position, 1)) BETWEEN 8192 AND 8202
864
+ )
865
+ BEGIN
866
+ SELECT RAISE(ABORT, 'log tag name is invalid');
867
+ END;
868
+
693
869
  CREATE INDEX IF NOT EXISTS log_tags_tag ON log_tags (tag);
694
870
 
695
- -- Column-scoped immutability: the original action's identity and target
696
- -- never change; the proposal lifecycle is allowed to mutate state,
697
- -- outcome, status_rx, rx, expanded.
871
+ -- Signal-derived classification is part of the log-row insert, not a later JS
872
+ -- write. This keeps the operation, its ambient occurrence, and its complete
873
+ -- initial folksonomy inside one SQLite commit.
874
+ CREATE TRIGGER IF NOT EXISTS log_entries_classify_signal
875
+ AFTER INSERT ON log_entries
876
+ WHEN NEW.op IN ('FIND', 'READ', 'EDIT', 'COPY', 'MOVE', 'BARE')
877
+ AND NEW.signal IS NOT NULL
878
+ BEGIN
879
+ SELECT CASE
880
+ WHEN json_type(NEW.signal) != 'array'
881
+ OR EXISTS (
882
+ SELECT 1 FROM json_each(NEW.signal)
883
+ WHERE type != 'text'
884
+ OR length(value) = 0
885
+ OR substr(value, 1, 1) = '-'
886
+ OR (
887
+ substr(value, 1, 1) = '+'
888
+ AND (length(value) < 2 OR substr(value, 2, 1) IN ('+', '-'))
889
+ )
890
+ OR instr(value, '[') > 0
891
+ OR instr(value, ']') > 0
892
+ OR instr(value, ',') > 0
893
+ OR instr(value, char(0)) > 0
894
+ OR EXISTS (
895
+ WITH RECURSIVE offsets(position) AS (
896
+ SELECT 1
897
+ UNION ALL
898
+ SELECT position + 1 FROM offsets WHERE position < length(value)
899
+ )
900
+ SELECT 1
901
+ FROM offsets
902
+ WHERE unicode(substr(value, position, 1)) BETWEEN 0 AND 32
903
+ OR unicode(substr(value, position, 1)) BETWEEN 127 AND 159
904
+ OR unicode(substr(value, position, 1)) IN (160, 5760, 8232, 8233, 8239, 8287, 12288, 65279)
905
+ OR unicode(substr(value, position, 1)) BETWEEN 8192 AND 8202
906
+ )
907
+ )
908
+ THEN RAISE(ABORT, 'classifying log operation signal accepts only tag or +tag additions')
909
+ END;
910
+ INSERT OR IGNORE INTO log_tags (log_entry_id, tag)
911
+ SELECT NEW.id, CASE WHEN substr(value, 1, 1) = '+' THEN substr(value, 2) ELSE value END
912
+ FROM json_each(NEW.signal);
913
+
914
+ -- The ambient-event and classification AFTER INSERT triggers may run in
915
+ -- either order. If the event already exists, finish its initial
916
+ -- snapshot here; otherwise its own trigger aggregates these rows later.
917
+ UPDATE ambient_events
918
+ SET tags = COALESCE((
919
+ SELECT json_group_array(tag)
920
+ FROM (SELECT tag FROM log_tags WHERE log_entry_id = NEW.id ORDER BY tag)
921
+ ), '[]')
922
+ WHERE kind = 'edit'
923
+ AND producer_worker_id = NEW.worker_id
924
+ AND source_record_id = NEW.id;
925
+ END;
926
+
927
+ -- Successful OPEN/FOLD rows are durable curation events even though their
928
+ -- ordinary packet projection is suppressed ({§fold-open-meta-operations}).
929
+ -- Preserve the exact selected set and each target's pre-event visibility so a
930
+ -- broad selector never collapses into the lossy fact `matched: N`.
931
+ CREATE TABLE IF NOT EXISTS log_curation_effects (
932
+ operation_log_entry_id INTEGER NOT NULL,
933
+ target_log_entry_id INTEGER NOT NULL,
934
+ expanded_before INTEGER NOT NULL CHECK (expanded_before IN (0, 1)),
935
+ tags_added TEXT NOT NULL DEFAULT '[]'
936
+ CHECK (json_valid(tags_added) AND json_type(tags_added) = 'array'),
937
+ tags_removed TEXT NOT NULL DEFAULT '[]'
938
+ CHECK (json_valid(tags_removed) AND json_type(tags_removed) = 'array'),
939
+ PRIMARY KEY (operation_log_entry_id, target_log_entry_id),
940
+ CHECK (operation_log_entry_id != target_log_entry_id),
941
+ FOREIGN KEY (operation_log_entry_id) REFERENCES log_entries(id) ON DELETE CASCADE,
942
+ FOREIGN KEY (target_log_entry_id) REFERENCES log_entries(id) ON DELETE CASCADE
943
+ ) STRICT, WITHOUT ROWID;
944
+
945
+ CREATE INDEX IF NOT EXISTS log_curation_effects_target
946
+ ON log_curation_effects (target_log_entry_id);
947
+
948
+ -- Make an invalid curation record structurally unavailable: the event must be
949
+ -- one successful OPEN/FOLD row, both rows must belong to the same worker, and
950
+ -- both exact delta sets must contain only unique canonical tag identities.
951
+ CREATE TRIGGER IF NOT EXISTS log_curation_effects_valid
952
+ BEFORE INSERT ON log_curation_effects
953
+ BEGIN
954
+ SELECT CASE WHEN
955
+ NOT EXISTS (
956
+ SELECT 1
957
+ FROM log_entries operation
958
+ JOIN log_entries target ON target.id = NEW.target_log_entry_id
959
+ WHERE operation.id = NEW.operation_log_entry_id
960
+ AND operation.op IN ('OPEN', 'FOLD')
961
+ AND operation.status_rx < 400
962
+ AND operation.worker_id = target.worker_id
963
+ )
964
+ OR EXISTS (
965
+ SELECT 1
966
+ FROM (
967
+ SELECT value, type FROM json_each(NEW.tags_added)
968
+ UNION ALL
969
+ SELECT value, type FROM json_each(NEW.tags_removed)
970
+ ) tag
971
+ WHERE tag.type != 'text'
972
+ OR length(tag.value) = 0
973
+ OR substr(tag.value, 1, 1) IN ('+', '-')
974
+ OR instr(tag.value, '[') > 0
975
+ OR instr(tag.value, ']') > 0
976
+ OR instr(tag.value, ',') > 0
977
+ OR instr(tag.value, char(0)) > 0
978
+ OR EXISTS (
979
+ WITH RECURSIVE offsets(position) AS (
980
+ SELECT 1
981
+ UNION ALL
982
+ SELECT position + 1 FROM offsets WHERE position < length(tag.value)
983
+ )
984
+ SELECT 1
985
+ FROM offsets
986
+ WHERE unicode(substr(tag.value, position, 1)) BETWEEN 0 AND 32
987
+ OR unicode(substr(tag.value, position, 1)) BETWEEN 127 AND 159
988
+ OR unicode(substr(tag.value, position, 1)) IN (160, 5760, 8232, 8233, 8239, 8287, 12288, 65279)
989
+ OR unicode(substr(tag.value, position, 1)) BETWEEN 8192 AND 8202
990
+ )
991
+ )
992
+ OR (SELECT COUNT(*) FROM json_each(NEW.tags_added))
993
+ != (SELECT COUNT(DISTINCT value) FROM json_each(NEW.tags_added))
994
+ OR (SELECT COUNT(*) FROM json_each(NEW.tags_removed))
995
+ != (SELECT COUNT(DISTINCT value) FROM json_each(NEW.tags_removed))
996
+ OR EXISTS (
997
+ SELECT 1
998
+ FROM json_each(NEW.tags_added) addition
999
+ JOIN json_each(NEW.tags_removed) removal ON removal.value = addition.value
1000
+ )
1001
+ THEN RAISE(ABORT, 'invalid log curation effect') END;
1002
+ END;
1003
+
1004
+ -- The dispatcher binds an exact, transient curation plan into attrs on the
1005
+ -- successful OPEN/FOLD row. No other row may carry that private payload.
1006
+ CREATE TRIGGER IF NOT EXISTS log_entries_curation_payload_valid
1007
+ BEFORE INSERT ON log_entries
1008
+ WHEN json_type(NEW.attrs, '$.__plurnk_curation') IS NOT NULL
1009
+ AND NOT (
1010
+ NEW.op IN ('OPEN', 'FOLD')
1011
+ AND NEW.status_rx < 400
1012
+ AND json_type(NEW.attrs, '$.__plurnk_curation') = 'object'
1013
+ )
1014
+ BEGIN
1015
+ SELECT RAISE(ABORT, 'private log curation payload requires a successful OPEN/FOLD row');
1016
+ END;
1017
+
1018
+ -- 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
1020
+ -- resulting classifications. Trigger failure rolls the operation row and all
1021
+ -- effects back together. The private plan is erased before INSERT returns.
1022
+ CREATE TRIGGER IF NOT EXISTS log_entries_apply_curation
1023
+ AFTER INSERT ON log_entries
1024
+ WHEN NEW.op IN ('OPEN', 'FOLD')
1025
+ AND NEW.status_rx < 400
1026
+ AND json_type(NEW.attrs, '$.__plurnk_curation') = 'object'
1027
+ BEGIN
1028
+ SELECT CASE WHEN
1029
+ COALESCE(json_type(NEW.attrs, '$.__plurnk_curation.ids'), '') != 'array'
1030
+ OR COALESCE(json_type(NEW.attrs, '$.__plurnk_curation.add'), '') != 'array'
1031
+ 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
1035
+ OR EXISTS (
1036
+ SELECT 1 FROM json_each(NEW.attrs, '$.__plurnk_curation.ids')
1037
+ WHERE type != 'integer' OR value <= 0
1038
+ )
1039
+ OR (
1040
+ SELECT COUNT(*) FROM json_each(NEW.attrs, '$.__plurnk_curation.ids')
1041
+ ) != (
1042
+ SELECT COUNT(DISTINCT value) FROM json_each(NEW.attrs, '$.__plurnk_curation.ids')
1043
+ )
1044
+ OR EXISTS (
1045
+ 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
1049
+ )
1050
+ OR EXISTS (
1051
+ SELECT 1
1052
+ FROM (
1053
+ SELECT value, type FROM json_each(NEW.attrs, '$.__plurnk_curation.add')
1054
+ UNION ALL
1055
+ SELECT value, type FROM json_each(NEW.attrs, '$.__plurnk_curation.remove')
1056
+ ) tag
1057
+ WHERE tag.type != 'text'
1058
+ OR length(tag.value) = 0
1059
+ OR substr(tag.value, 1, 1) IN ('+', '-')
1060
+ OR instr(tag.value, '[') > 0
1061
+ OR instr(tag.value, ']') > 0
1062
+ OR instr(tag.value, ',') > 0
1063
+ OR instr(tag.value, char(0)) > 0
1064
+ OR EXISTS (
1065
+ WITH RECURSIVE offsets(position) AS (
1066
+ SELECT 1
1067
+ UNION ALL
1068
+ SELECT position + 1 FROM offsets WHERE position < length(tag.value)
1069
+ )
1070
+ SELECT 1
1071
+ FROM offsets
1072
+ WHERE unicode(substr(tag.value, position, 1)) BETWEEN 0 AND 32
1073
+ OR unicode(substr(tag.value, position, 1)) BETWEEN 127 AND 159
1074
+ OR unicode(substr(tag.value, position, 1)) IN (160, 5760, 8232, 8233, 8239, 8287, 12288, 65279)
1075
+ OR unicode(substr(tag.value, position, 1)) BETWEEN 8192 AND 8202
1076
+ )
1077
+ )
1078
+ OR (
1079
+ SELECT COUNT(*) FROM json_each(NEW.attrs, '$.__plurnk_curation.add')
1080
+ ) != (
1081
+ SELECT COUNT(DISTINCT value) FROM json_each(NEW.attrs, '$.__plurnk_curation.add')
1082
+ )
1083
+ OR (
1084
+ SELECT COUNT(*) FROM json_each(NEW.attrs, '$.__plurnk_curation.remove')
1085
+ ) != (
1086
+ SELECT COUNT(DISTINCT value) FROM json_each(NEW.attrs, '$.__plurnk_curation.remove')
1087
+ )
1088
+ OR EXISTS (
1089
+ SELECT 1
1090
+ FROM json_each(NEW.attrs, '$.__plurnk_curation.add') addition
1091
+ JOIN json_each(NEW.attrs, '$.__plurnk_curation.remove') removal
1092
+ ON removal.value = addition.value
1093
+ )
1094
+ THEN RAISE(ABORT, 'invalid private log curation payload') END;
1095
+
1096
+ INSERT INTO log_curation_effects (
1097
+ operation_log_entry_id,
1098
+ target_log_entry_id,
1099
+ expanded_before,
1100
+ tags_added,
1101
+ tags_removed
1102
+ )
1103
+ SELECT
1104
+ NEW.id,
1105
+ target.id,
1106
+ target.expanded,
1107
+ COALESCE((
1108
+ SELECT json_group_array(ordered.tag)
1109
+ FROM (
1110
+ SELECT addition.value AS tag
1111
+ FROM json_each(NEW.attrs, '$.__plurnk_curation.add') addition
1112
+ WHERE NOT EXISTS (
1113
+ SELECT 1 FROM log_tags
1114
+ WHERE log_entry_id = target.id AND tag = addition.value
1115
+ )
1116
+ ORDER BY addition.value
1117
+ ) ordered
1118
+ ), '[]'),
1119
+ COALESCE((
1120
+ SELECT json_group_array(ordered.tag)
1121
+ FROM (
1122
+ SELECT removal.value AS tag
1123
+ FROM json_each(NEW.attrs, '$.__plurnk_curation.remove') removal
1124
+ WHERE EXISTS (
1125
+ SELECT 1 FROM log_tags
1126
+ WHERE log_entry_id = target.id AND tag = removal.value
1127
+ )
1128
+ ORDER BY removal.value
1129
+ ) ordered
1130
+ ), '[]')
1131
+ FROM json_each(NEW.attrs, '$.__plurnk_curation.ids') selected
1132
+ JOIN log_entries target ON target.id = selected.value;
1133
+
1134
+ UPDATE log_entries
1135
+ SET expanded = json_extract(NEW.attrs, '$.__plurnk_curation.expanded')
1136
+ WHERE id IN (
1137
+ SELECT value FROM json_each(NEW.attrs, '$.__plurnk_curation.ids')
1138
+ );
1139
+
1140
+ DELETE FROM log_tags
1141
+ WHERE log_entry_id IN (
1142
+ SELECT value FROM json_each(NEW.attrs, '$.__plurnk_curation.ids')
1143
+ )
1144
+ AND tag IN (
1145
+ SELECT value FROM json_each(NEW.attrs, '$.__plurnk_curation.remove')
1146
+ );
1147
+
1148
+ 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
1151
+ CROSS JOIN json_each(NEW.attrs, '$.__plurnk_curation.add') addition;
1152
+
1153
+ UPDATE log_entries
1154
+ SET attrs = json_remove(attrs, '$.__plurnk_curation')
1155
+ WHERE id = NEW.id;
1156
+ END;
1157
+
1158
+ -- Column-scoped immutability: the original action's identity and target never
1159
+ -- change; the proposal lifecycle is allowed to mutate state, outcome,
1160
+ -- status_rx, rx, expanded. Keep attrs separate so the curation trigger's one
1161
+ -- private-payload removal cannot exempt changes to any other core column.
698
1162
  CREATE TRIGGER IF NOT EXISTS log_entries_immutable_core
699
1163
  BEFORE UPDATE OF
700
- worker_id, loop_id, turn_id, sequence, at, origin, source,
1164
+ worker_id, loop_id, turn_id, sequence, at, origin, source, model_call_id,
701
1165
  op, suffix, signal,
702
1166
  scheme, username, password, hostname,
703
1167
  port, pathname, query, fragment,
704
- lineMarker, tx, mimetype_tx, mimetype_rx, attrs
1168
+ lineMarker, tx, mimetype_tx, mimetype_rx
705
1169
  ON log_entries
706
1170
  BEGIN
707
1171
  SELECT RAISE(ABORT, 'log_entries core fields are immutable; only state/outcome/status_rx/rx/expanded may change');
708
1172
  END;
709
1173
 
1174
+ CREATE TRIGGER IF NOT EXISTS log_entries_immutable_attrs
1175
+ BEFORE UPDATE OF attrs ON log_entries
1176
+ WHEN COALESCE((
1177
+ json_type(OLD.attrs, '$.__plurnk_curation') = 'object'
1178
+ AND NEW.attrs = json_remove(OLD.attrs, '$.__plurnk_curation')
1179
+ ), 0) = 0
1180
+ BEGIN
1181
+ SELECT RAISE(ABORT, 'log_entries attrs are immutable outside curation payload removal');
1182
+ END;
1183
+
710
1184
  -- The engine may stamp an originating row exactly once. No later reassignment
711
1185
  -- can sever or counterfeit the occurrence identity.
712
1186
  CREATE TRIGGER IF NOT EXISTS log_entries_ambient_event_once
@@ -734,10 +1208,17 @@ WHEN NEW.ambient_event_id IS NULL
734
1208
  BEGIN
735
1209
  INSERT INTO ambient_events (
736
1210
  workspace_id, producer_worker_id, kind, source_record_id, source,
737
- op, scheme, hostname, pathname, rx, attrs, status_rx
1211
+ op, scheme, hostname, pathname, rx, attrs, tags, status_rx
738
1212
  )
739
1213
  SELECT w.workspace_id, NEW.worker_id, 'edit', NEW.id, NEW.source,
740
- 'EDIT', NEW.scheme, NEW.hostname, NEW.pathname, NEW.rx, NEW.attrs, NEW.status_rx
1214
+ 'EDIT', NEW.scheme, NEW.hostname, NEW.pathname, NEW.rx, NEW.attrs,
1215
+ COALESCE((
1216
+ SELECT json_group_array(ordered.tag)
1217
+ FROM (
1218
+ SELECT tag FROM log_tags WHERE log_entry_id = NEW.id ORDER BY tag
1219
+ ) ordered
1220
+ ), '[]'),
1221
+ NEW.status_rx
741
1222
  FROM workers w
742
1223
  WHERE w.id = NEW.worker_id;
743
1224
  UPDATE log_entries SET ambient_event_id = last_insert_rowid() WHERE id = NEW.id;
@@ -761,97 +1242,22 @@ WHEN OLD.state = 'proposed'
761
1242
  BEGIN
762
1243
  INSERT INTO ambient_events (
763
1244
  workspace_id, producer_worker_id, kind, source_record_id, source,
764
- op, scheme, hostname, pathname, rx, attrs, status_rx
1245
+ op, scheme, hostname, pathname, rx, attrs, tags, status_rx
765
1246
  )
766
1247
  SELECT w.workspace_id, NEW.worker_id, 'edit', NEW.id, NEW.source,
767
- 'EDIT', NEW.scheme, NEW.hostname, NEW.pathname, NEW.rx, NEW.attrs, NEW.status_rx
1248
+ 'EDIT', NEW.scheme, NEW.hostname, NEW.pathname, NEW.rx, NEW.attrs,
1249
+ COALESCE((
1250
+ SELECT json_group_array(ordered.tag)
1251
+ FROM (
1252
+ SELECT tag FROM log_tags WHERE log_entry_id = NEW.id ORDER BY tag
1253
+ ) ordered
1254
+ ), '[]'),
1255
+ NEW.status_rx
768
1256
  FROM workers w
769
1257
  WHERE w.id = NEW.worker_id;
770
1258
  UPDATE log_entries SET ambient_event_id = last_insert_rowid() WHERE id = NEW.id;
771
1259
  END;
772
1260
 
773
- -- One loop's cost is the complete sum of its provider attempts. One unknown
774
- -- contributing turn makes the aggregate unknown rather than silently zero.
775
- CREATE VIEW IF NOT EXISTS loop_costs AS
776
- SELECT l.id, l.worker_id,
777
- CASE
778
- WHEN COUNT(t.id) FILTER (WHERE t.usage_cost_usd IS NULL) > 0 THEN NULL
779
- ELSE COALESCE(SUM(t.usage_cost_usd), 0)
780
- END AS cost_usd
781
- FROM loops l
782
- LEFT JOIN turns t ON t.loop_id = l.id
783
- GROUP BY l.id;
784
-
785
- -- cost_rollups
786
- -- Triggers maintaining denormalized USD totals on workers and workspaces
787
- -- as turns insert/update. Pure denormalization (textbook trigger use);
788
- -- no branching state-machine logic lives here.
789
- CREATE TRIGGER IF NOT EXISTS turns_cost_rollup_insert_worker
790
- AFTER INSERT ON turns
791
- BEGIN
792
- UPDATE workers
793
- SET cost_usd = (
794
- SELECT CASE WHEN COUNT(*) FILTER (WHERE lc.cost_usd IS NULL) > 0
795
- THEN NULL ELSE COALESCE(SUM(lc.cost_usd), 0) END
796
- FROM loop_costs lc
797
- WHERE lc.worker_id = workers.id
798
- )
799
- WHERE id = (SELECT worker_id FROM loops WHERE id = NEW.loop_id);
800
- END;
801
-
802
- CREATE TRIGGER IF NOT EXISTS turns_cost_rollup_insert_workspace
803
- AFTER INSERT ON turns
804
- BEGIN
805
- UPDATE workspaces
806
- SET cost_usd = (
807
- SELECT CASE WHEN COUNT(*) FILTER (WHERE lc.cost_usd IS NULL) > 0
808
- THEN NULL ELSE COALESCE(SUM(lc.cost_usd), 0) END
809
- FROM loop_costs lc
810
- JOIN workers r ON r.id = lc.worker_id
811
- WHERE r.workspace_id = workspaces.id
812
- )
813
- WHERE id = (
814
- SELECT r.workspace_id
815
- FROM workers r
816
- JOIN loops l ON l.worker_id = r.id
817
- WHERE l.id = NEW.loop_id
818
- );
819
- END;
820
-
821
- CREATE TRIGGER IF NOT EXISTS turns_cost_rollup_update_worker
822
- AFTER UPDATE OF usage_cost_usd ON turns
823
- WHEN NEW.usage_cost_usd IS NOT OLD.usage_cost_usd
824
- BEGIN
825
- UPDATE workers
826
- SET cost_usd = (
827
- SELECT CASE WHEN COUNT(*) FILTER (WHERE lc.cost_usd IS NULL) > 0
828
- THEN NULL ELSE COALESCE(SUM(lc.cost_usd), 0) END
829
- FROM loop_costs lc
830
- WHERE lc.worker_id = workers.id
831
- )
832
- WHERE id = (SELECT worker_id FROM loops WHERE id = NEW.loop_id);
833
- END;
834
-
835
- CREATE TRIGGER IF NOT EXISTS turns_cost_rollup_update_workspace
836
- AFTER UPDATE OF usage_cost_usd ON turns
837
- WHEN NEW.usage_cost_usd IS NOT OLD.usage_cost_usd
838
- BEGIN
839
- UPDATE workspaces
840
- SET cost_usd = (
841
- SELECT CASE WHEN COUNT(*) FILTER (WHERE lc.cost_usd IS NULL) > 0
842
- THEN NULL ELSE COALESCE(SUM(lc.cost_usd), 0) END
843
- FROM loop_costs lc
844
- JOIN workers r ON r.id = lc.worker_id
845
- WHERE r.workspace_id = workspaces.id
846
- )
847
- WHERE id = (
848
- SELECT r.workspace_id
849
- FROM workers r
850
- JOIN loops l ON l.worker_id = r.id
851
- WHERE l.id = NEW.loop_id
852
- );
853
- END;
854
-
855
1261
  -- subscriptions
856
1262
  -- Durable subscription lifecycle per SPEC {§subscriptions}. The row records what
857
1263
  -- the worker holds and routes cancellation to a separate process-local callable;