@plurnk/plurnk-service 1.5.0 → 1.6.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 (201) hide show
  1. package/.env.defaults +21 -12
  2. package/INSTALL.md +11 -7
  3. package/SPEC.md +437 -304
  4. package/dist/build-info.json +1 -1
  5. package/dist/content/edit-collision.d.ts +5 -0
  6. package/dist/content/edit-collision.d.ts.map +1 -0
  7. package/dist/content/edit-collision.js +14 -0
  8. package/dist/content/edit-collision.js.map +1 -0
  9. package/dist/content/edit-receipt.d.ts +3 -2
  10. package/dist/content/edit-receipt.d.ts.map +1 -1
  11. package/dist/content/edit-receipt.js +58 -13
  12. package/dist/content/edit-receipt.js.map +1 -1
  13. package/dist/content/index.d.ts +4 -1
  14. package/dist/content/index.d.ts.map +1 -1
  15. package/dist/content/index.js +3 -1
  16. package/dist/content/index.js.map +1 -1
  17. package/dist/content/line-anchors.d.ts +41 -0
  18. package/dist/content/line-anchors.d.ts.map +1 -0
  19. package/dist/content/line-anchors.js +191 -0
  20. package/dist/content/line-anchors.js.map +1 -0
  21. package/dist/content/read-projector.d.ts +7 -1
  22. package/dist/content/read-projector.d.ts.map +1 -1
  23. package/dist/content/read-projector.js +46 -10
  24. package/dist/content/read-projector.js.map +1 -1
  25. package/dist/content/read-resolve.js +3 -3
  26. package/dist/content/read-resolve.js.map +1 -1
  27. package/dist/core/BudgetReadout.d.ts.map +1 -1
  28. package/dist/core/BudgetReadout.js +9 -2
  29. package/dist/core/BudgetReadout.js.map +1 -1
  30. package/dist/core/ChannelWrite.js +1 -1
  31. package/dist/core/ChannelWrite.js.map +1 -1
  32. package/dist/core/Dispatcher.d.ts +13 -3
  33. package/dist/core/Dispatcher.d.ts.map +1 -1
  34. package/dist/core/Dispatcher.js +391 -113
  35. package/dist/core/Dispatcher.js.map +1 -1
  36. package/dist/core/DurableStatement.d.ts.map +1 -1
  37. package/dist/core/DurableStatement.js +2 -0
  38. package/dist/core/DurableStatement.js.map +1 -1
  39. package/dist/core/Engine.d.ts +13 -14
  40. package/dist/core/Engine.d.ts.map +1 -1
  41. package/dist/core/Engine.js +434 -300
  42. package/dist/core/Engine.js.map +1 -1
  43. package/dist/core/Engine.sql +156 -90
  44. package/dist/core/ExecutableTools.d.ts +10 -0
  45. package/dist/core/ExecutableTools.d.ts.map +1 -0
  46. package/dist/core/ExecutableTools.js +41 -0
  47. package/dist/core/ExecutableTools.js.map +1 -0
  48. package/dist/core/ExecutorRegistry.d.ts +3 -3
  49. package/dist/core/ExecutorRegistry.d.ts.map +1 -1
  50. package/dist/core/ExecutorRegistry.js +2 -2
  51. package/dist/core/ExecutorRegistry.js.map +1 -1
  52. package/dist/core/LogBody.d.ts +2 -0
  53. package/dist/core/LogBody.d.ts.map +1 -1
  54. package/dist/core/LogBody.js +17 -5
  55. package/dist/core/LogBody.js.map +1 -1
  56. package/dist/core/LogEntryProjection.d.ts +14 -0
  57. package/dist/core/LogEntryProjection.d.ts.map +1 -0
  58. package/dist/core/LogEntryProjection.js +38 -0
  59. package/dist/core/LogEntryProjection.js.map +1 -0
  60. package/dist/core/ModelCall.d.ts +27 -0
  61. package/dist/core/ModelCall.d.ts.map +1 -0
  62. package/dist/core/ModelCall.js +125 -0
  63. package/dist/core/ModelCall.js.map +1 -0
  64. package/dist/core/PacketBuilder.d.ts +10 -10
  65. package/dist/core/PacketBuilder.d.ts.map +1 -1
  66. package/dist/core/PacketBuilder.js +131 -103
  67. package/dist/core/PacketBuilder.js.map +1 -1
  68. package/dist/core/ProblemLog.d.ts.map +1 -1
  69. package/dist/core/ProblemLog.js +1 -0
  70. package/dist/core/ProblemLog.js.map +1 -1
  71. package/dist/core/ProposalLifecycle.d.ts.map +1 -1
  72. package/dist/core/ProposalLifecycle.js +3 -4
  73. package/dist/core/ProposalLifecycle.js.map +1 -1
  74. package/dist/core/ProviderInstantiate.d.ts.map +1 -1
  75. package/dist/core/ProviderInstantiate.js +23 -6
  76. package/dist/core/ProviderInstantiate.js.map +1 -1
  77. package/dist/core/SchemeRegistry.js +7 -7
  78. package/dist/core/SchemeRegistry.js.map +1 -1
  79. package/dist/core/StrikeRail.d.ts +0 -1
  80. package/dist/core/StrikeRail.d.ts.map +1 -1
  81. package/dist/core/StrikeRail.js +6 -4
  82. package/dist/core/StrikeRail.js.map +1 -1
  83. package/dist/core/caps/CapsResolve.js +1 -1
  84. package/dist/core/caps/CapsResolve.js.map +1 -1
  85. package/dist/core/caps/DbEntryCaps.d.ts +2 -1
  86. package/dist/core/caps/DbEntryCaps.d.ts.map +1 -1
  87. package/dist/core/caps/DbEntryCaps.js +8 -5
  88. package/dist/core/caps/DbEntryCaps.js.map +1 -1
  89. package/dist/core/caps/DbProjectionCaps.d.ts +1 -0
  90. package/dist/core/caps/DbProjectionCaps.d.ts.map +1 -1
  91. package/dist/core/caps/DbProjectionCaps.js +29 -0
  92. package/dist/core/caps/DbProjectionCaps.js.map +1 -1
  93. package/dist/core/caps/SchemeCtxImpl.d.ts +5 -2
  94. package/dist/core/caps/SchemeCtxImpl.d.ts.map +1 -1
  95. package/dist/core/caps/SchemeCtxImpl.js +6 -4
  96. package/dist/core/caps/SchemeCtxImpl.js.map +1 -1
  97. package/dist/core/fork.d.ts.map +1 -1
  98. package/dist/core/fork.js +23 -7
  99. package/dist/core/fork.js.map +1 -1
  100. package/dist/core/fork.sql +33 -12
  101. package/dist/core/git-membership.d.ts.map +1 -1
  102. package/dist/core/git-membership.js +1 -2
  103. package/dist/core/git-membership.js.map +1 -1
  104. package/dist/core/git-state.js +1 -1
  105. package/dist/core/git-state.js.map +1 -1
  106. package/dist/core/packet-wire.d.ts +4 -1
  107. package/dist/core/packet-wire.d.ts.map +1 -1
  108. package/dist/core/packet-wire.js +248 -90
  109. package/dist/core/packet-wire.js.map +1 -1
  110. package/dist/core/provider-accounting.d.ts +24 -0
  111. package/dist/core/provider-accounting.d.ts.map +1 -0
  112. package/dist/core/provider-accounting.js +76 -0
  113. package/dist/core/provider-accounting.js.map +1 -0
  114. package/dist/core/search-gate.d.ts +2 -2
  115. package/dist/core/search-gate.d.ts.map +1 -1
  116. package/dist/core/search-gate.js +8 -8
  117. package/dist/core/search-gate.js.map +1 -1
  118. package/dist/core/token-ruler.js +1 -1
  119. package/dist/core/token-ruler.js.map +1 -1
  120. package/dist/core/turn-scheduler.js +1 -1
  121. package/dist/core/turn-scheduler.js.map +1 -1
  122. package/dist/digest/Digest.d.ts.map +1 -1
  123. package/dist/digest/Digest.js +220 -109
  124. package/dist/digest/Digest.js.map +1 -1
  125. package/dist/digest/digest.sql +40 -18
  126. package/dist/schemes/Exec.d.ts.map +1 -1
  127. package/dist/schemes/Exec.js +111 -101
  128. package/dist/schemes/Exec.js.map +1 -1
  129. package/dist/schemes/File.d.ts +5 -4
  130. package/dist/schemes/File.d.ts.map +1 -1
  131. package/dist/schemes/File.js +24 -22
  132. package/dist/schemes/File.js.map +1 -1
  133. package/dist/schemes/Log.d.ts +11 -0
  134. package/dist/schemes/Log.d.ts.map +1 -1
  135. package/dist/schemes/Log.js +171 -106
  136. package/dist/schemes/Log.js.map +1 -1
  137. package/dist/schemes/Log.sql +48 -32
  138. package/dist/schemes/Prompt.d.ts +3 -3
  139. package/dist/schemes/Prompt.d.ts.map +1 -1
  140. package/dist/schemes/Prompt.js +4 -1
  141. package/dist/schemes/Prompt.js.map +1 -1
  142. package/dist/schemes/Skill.d.ts +4 -4
  143. package/dist/schemes/Skill.d.ts.map +1 -1
  144. package/dist/schemes/Skill.js +3 -0
  145. package/dist/schemes/Skill.js.map +1 -1
  146. package/dist/schemes/Worker.d.ts +4 -4
  147. package/dist/schemes/Worker.d.ts.map +1 -1
  148. package/dist/schemes/Worker.js +9 -5
  149. package/dist/schemes/Worker.js.map +1 -1
  150. package/dist/schemes/_entry-crud.d.ts +0 -1
  151. package/dist/schemes/_entry-crud.d.ts.map +1 -1
  152. package/dist/schemes/_entry-crud.js +15 -9
  153. package/dist/schemes/_entry-crud.js.map +1 -1
  154. package/dist/schemes/_entry-crud.sql +2 -14
  155. package/dist/schemes/_entry-find.d.ts +13 -12
  156. package/dist/schemes/_entry-find.d.ts.map +1 -1
  157. package/dist/schemes/_entry-find.js +33 -28
  158. package/dist/schemes/_entry-find.js.map +1 -1
  159. package/dist/schemes/_entry-find.sql +2 -21
  160. package/dist/schemes/_entry-graph.sql +1 -1
  161. package/dist/schemes/_entry-manifest.d.ts +5 -17
  162. package/dist/schemes/_entry-manifest.d.ts.map +1 -1
  163. package/dist/schemes/_entry-manifest.js +28 -24
  164. package/dist/schemes/_entry-manifest.js.map +1 -1
  165. package/dist/schemes/_entry-ops.d.ts +5 -4
  166. package/dist/schemes/_entry-ops.d.ts.map +1 -1
  167. package/dist/schemes/_entry-ops.js +87 -47
  168. package/dist/schemes/_entry-ops.js.map +1 -1
  169. package/dist/schemes/_entry-ops.sql +30 -4
  170. package/dist/schemes/_entry-send.js +4 -4
  171. package/dist/schemes/_entry-send.js.map +1 -1
  172. package/dist/schemes/_search-index.d.ts.map +1 -1
  173. package/dist/schemes/_search-index.js +10 -2
  174. package/dist/schemes/_search-index.js.map +1 -1
  175. package/dist/schemes/exec-abort.js +2 -2
  176. package/dist/schemes/exec-abort.js.map +1 -1
  177. package/dist/server/Daemon.d.ts +1 -1
  178. package/dist/server/Daemon.d.ts.map +1 -1
  179. package/dist/server/Daemon.js +25 -23
  180. package/dist/server/Daemon.js.map +1 -1
  181. package/dist/server/drain.sql +2 -2
  182. package/dist/server/envelope.d.ts +0 -2
  183. package/dist/server/envelope.d.ts.map +1 -1
  184. package/dist/server/envelope.js.map +1 -1
  185. package/dist/server/envelope.sql +2 -2
  186. package/dist/server/lifecycle-recovery.sql +25 -15
  187. package/dist/server/logEntry.d.ts +1 -0
  188. package/dist/server/logEntry.d.ts.map +1 -1
  189. package/dist/server/logEntry.js +13 -1
  190. package/dist/server/logEntry.js.map +1 -1
  191. package/dist/server/logEntry.sql +5 -1
  192. package/migrations/001_schema.sql +603 -207
  193. package/package.json +39 -34
  194. package/dist/core/BudgetOverflow.d.ts +0 -20
  195. package/dist/core/BudgetOverflow.d.ts.map +0 -1
  196. package/dist/core/BudgetOverflow.js +0 -49
  197. package/dist/core/BudgetOverflow.js.map +0 -1
  198. package/dist/core/caps/DbTagCaps.d.ts +0 -10
  199. package/dist/core/caps/DbTagCaps.d.ts.map +0 -1
  200. package/dist/core/caps/DbTagCaps.js +0 -46
  201. package/dist/core/caps/DbTagCaps.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,12 +219,6 @@ 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
222
  -- Effective packet allowance for this turn's provider and policy; NULL is
218
223
  -- uncapped or unknown. {§tokenomics-client-gauge}
219
224
  usage_prompt_budget INTEGER CHECK (usage_prompt_budget IS NULL OR usage_prompt_budget >= 1),
@@ -258,148 +263,253 @@ 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)),
273
277
  -- Exact opaque tag set forwarded with this provider call.
274
278
  -- {§attribution}
275
279
  attributions TEXT NOT NULL DEFAULT '[]' CHECK (
276
280
  json_valid(attributions) AND json_type(attributions) = 'array'
277
281
  ),
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
282
  finish_reason TEXT,
285
283
  model TEXT NOT NULL CHECK (length(model) >= 1),
286
284
  timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
287
285
  completed_at TEXT,
288
286
  CHECK (
289
287
  (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
288
+ AND response IS NULL AND failure IS NULL
294
289
  AND finish_reason IS NULL AND completed_at IS NULL)
295
290
  OR
296
291
  (state = 'response'
297
292
  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)
293
+ AND completed_at IS NOT NULL)
301
294
  OR
302
295
  (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)
296
+ AND response IS NULL AND failure IS NOT NULL
297
+ AND completed_at IS NOT NULL)
307
298
  ),
308
299
  UNIQUE (turn_id, sequence),
309
300
  FOREIGN KEY (turn_id) REFERENCES turns(id) ON DELETE CASCADE
310
301
  ) STRICT;
311
302
 
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;
303
+ CREATE INDEX IF NOT EXISTS model_calls_turn_id ON model_calls (turn_id, sequence);
316
304
 
317
- CREATE TRIGGER IF NOT EXISTS turn_attempts_request_identity_immutable
318
- BEFORE UPDATE OF turn_id, sequence, attributions, timestamp ON turn_attempts
305
+ CREATE TRIGGER IF NOT EXISTS model_calls_request_identity_immutable
306
+ BEFORE UPDATE OF turn_id, sequence, kind, attributions, timestamp ON model_calls
319
307
  BEGIN
320
- SELECT RAISE(ABORT, 'provider attempt request identity is immutable');
308
+ SELECT RAISE(ABORT, 'model call request identity is immutable');
321
309
  END;
322
310
 
323
- CREATE TRIGGER IF NOT EXISTS turn_attempts_state_forward_only
324
- BEFORE UPDATE OF state ON turn_attempts
311
+ CREATE TRIGGER IF NOT EXISTS model_calls_state_forward_only
312
+ BEFORE UPDATE OF state ON model_calls
325
313
  WHEN NOT (
326
314
  NEW.state = OLD.state
327
315
  OR (OLD.state = 'pending' AND NEW.state IN ('response', 'error'))
328
316
  )
329
317
  BEGIN
330
- SELECT RAISE(ABORT, 'provider attempt state may only close once');
318
+ SELECT RAISE(ABORT, 'model call state may only close once');
331
319
  END;
332
320
 
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
321
+ CREATE TRIGGER IF NOT EXISTS model_calls_observation_immutable
322
+ BEFORE UPDATE OF response, failure, finish_reason, model, completed_at
323
+ ON model_calls
337
324
  WHEN OLD.state != 'pending'
338
325
  BEGIN
339
- SELECT RAISE(ABORT, 'provider attempt observation is immutable');
326
+ SELECT RAISE(ABORT, 'model call observation is immutable');
327
+ END;
328
+
329
+ -- Emission admission specializes one model call without re-owning its response,
330
+ -- provider identity, ordering, or accounting evidence.
331
+ CREATE TABLE IF NOT EXISTS turn_attempts (
332
+ id INTEGER NOT NULL PRIMARY KEY,
333
+ model_call_id INTEGER NOT NULL UNIQUE,
334
+ accepted INTEGER CHECK (accepted IS NULL OR accepted IN (0, 1)),
335
+ parse_errors TEXT NOT NULL DEFAULT '[]' CHECK (
336
+ json_valid(parse_errors) AND json_type(parse_errors) = 'array'
337
+ ),
338
+ FOREIGN KEY (model_call_id) REFERENCES model_calls(id) ON DELETE CASCADE
339
+ ) STRICT;
340
+
341
+ CREATE TRIGGER IF NOT EXISTS turn_attempts_request_identity_immutable
342
+ BEFORE UPDATE OF model_call_id ON turn_attempts
343
+ BEGIN
344
+ SELECT RAISE(ABORT, 'emission attempt identity is immutable');
345
+ END;
346
+
347
+ CREATE TRIGGER IF NOT EXISTS turn_attempts_emission_call_only
348
+ BEFORE INSERT ON turn_attempts
349
+ WHEN COALESCE((SELECT kind = 'emission' FROM model_calls WHERE id = NEW.model_call_id), 0) != 1
350
+ BEGIN
351
+ SELECT RAISE(ABORT, 'turn attempt requires an emission model call');
352
+ END;
353
+
354
+ CREATE TRIGGER IF NOT EXISTS turn_attempts_classification_after_response
355
+ BEFORE UPDATE OF accepted, parse_errors ON turn_attempts
356
+ WHEN COALESCE((SELECT state = 'response' FROM model_calls WHERE id = OLD.model_call_id), 0) != 1
357
+ BEGIN
358
+ SELECT RAISE(ABORT, 'emission classification requires model response evidence');
340
359
  END;
341
360
 
342
361
  CREATE TRIGGER IF NOT EXISTS turn_attempts_classification_once
343
- BEFORE UPDATE OF accepted, parse_errors, failure, usage_cost, usage_cost_usd
362
+ BEFORE UPDATE OF accepted, parse_errors
344
363
  ON turn_attempts
345
- WHEN OLD.state = 'error' OR (OLD.state = 'response' AND OLD.accepted IS NOT NULL)
364
+ WHEN OLD.accepted IS NOT NULL
346
365
  BEGIN
347
- SELECT RAISE(ABORT, 'provider attempt classification is immutable');
366
+ SELECT RAISE(ABORT, 'emission attempt classification is immutable');
348
367
  END;
349
368
 
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
369
+ CREATE TRIGGER IF NOT EXISTS turn_attempts_one_accepted_insert
370
+ BEFORE INSERT ON turn_attempts
371
+ WHEN NEW.accepted = 1 AND EXISTS (
372
+ SELECT 1
373
+ FROM turn_attempts existing
374
+ JOIN model_calls old_call ON old_call.id = existing.model_call_id
375
+ JOIN model_calls new_call ON new_call.id = NEW.model_call_id
376
+ WHERE old_call.turn_id = new_call.turn_id AND existing.accepted = 1
377
+ )
379
378
  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;
379
+ SELECT RAISE(ABORT, 'one emission may be accepted per turn');
388
380
  END;
389
381
 
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
382
+ CREATE TRIGGER IF NOT EXISTS turn_attempts_one_accepted_update
383
+ BEFORE UPDATE OF accepted ON turn_attempts
384
+ WHEN NEW.accepted = 1 AND EXISTS (
385
+ SELECT 1
386
+ FROM turn_attempts existing
387
+ JOIN model_calls old_call ON old_call.id = existing.model_call_id
388
+ JOIN model_calls new_call ON new_call.id = NEW.model_call_id
389
+ WHERE old_call.turn_id = new_call.turn_id
390
+ AND existing.id != OLD.id
391
+ AND existing.accepted = 1
392
+ )
393
+ BEGIN
394
+ SELECT RAISE(ABORT, 'one emission may be accepted per turn');
395
+ END;
396
+
397
+ -- {§provider-request-accounting}: one row is opened before one physical
398
+ -- provider request and settled exactly once with the evidence from that request.
399
+ -- Monetary values remain canonical decimal strings; SQLite REAL is never an
400
+ -- accounting representation.
401
+ CREATE TABLE IF NOT EXISTS provider_requests (
402
+ id INTEGER NOT NULL PRIMARY KEY,
403
+ model_call_id INTEGER NOT NULL,
404
+ sequence INTEGER NOT NULL CHECK (sequence >= 1),
405
+ provider TEXT NOT NULL CHECK (length(provider) > 0),
406
+ model TEXT NOT NULL CHECK (length(model) > 0),
407
+ state TEXT NOT NULL DEFAULT 'pending' CHECK (state IN ('pending', 'settled')),
408
+ outcome TEXT CHECK (outcome IN ('response', 'error')),
409
+ status INTEGER CHECK (status IS NULL OR status BETWEEN 100 AND 599),
410
+ usage_input INTEGER CHECK (usage_input IS NULL OR usage_input >= 0),
411
+ usage_output INTEGER CHECK (usage_output IS NULL OR usage_output >= 0),
412
+ usage_total INTEGER CHECK (usage_total IS NULL OR usage_total >= 0),
413
+ usage_input_no_cache INTEGER CHECK (usage_input_no_cache IS NULL OR usage_input_no_cache >= 0),
414
+ usage_input_cache_read INTEGER CHECK (usage_input_cache_read IS NULL OR usage_input_cache_read >= 0),
415
+ usage_input_cache_write INTEGER CHECK (usage_input_cache_write IS NULL OR usage_input_cache_write >= 0),
416
+ usage_output_text INTEGER CHECK (usage_output_text IS NULL OR usage_output_text >= 0),
417
+ usage_output_reasoning INTEGER CHECK (usage_output_reasoning IS NULL OR usage_output_reasoning >= 0),
418
+ cost_kind TEXT CHECK (cost_kind IN ('charged', 'estimated', 'unknown')),
419
+ cost_amount TEXT,
420
+ cost_currency TEXT,
421
+ cost_usd_equivalent TEXT,
422
+ cost_source TEXT,
423
+ cost_reason TEXT,
424
+ started_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
425
+ completed_at TEXT,
426
+ CHECK (
427
+ (state = 'pending'
428
+ AND outcome IS NULL AND status IS NULL
429
+ AND usage_input IS NULL AND usage_output IS NULL AND usage_total IS NULL
430
+ AND usage_input_no_cache IS NULL AND usage_input_cache_read IS NULL
431
+ AND usage_input_cache_write IS NULL AND usage_output_text IS NULL
432
+ AND usage_output_reasoning IS NULL
433
+ AND cost_kind IS NULL AND cost_amount IS NULL AND cost_currency IS NULL
434
+ AND cost_usd_equivalent IS NULL AND cost_source IS NULL AND cost_reason IS NULL
435
+ AND completed_at IS NULL)
436
+ OR
437
+ (state = 'settled' AND outcome IS NOT NULL AND cost_kind IS NOT NULL AND completed_at IS NOT NULL)
438
+ ),
439
+ CHECK (
440
+ cost_amount IS NULL OR (
441
+ length(cost_amount) > 0
442
+ AND cost_amount NOT GLOB '*[^0-9.]*'
443
+ AND cost_amount NOT LIKE '.%'
444
+ AND cost_amount NOT LIKE '%.'
445
+ AND cost_amount NOT LIKE '%.%.%'
446
+ AND (cost_amount = '0' OR cost_amount NOT GLOB '0[0-9]*')
447
+ )
448
+ ),
449
+ CHECK (
450
+ cost_usd_equivalent IS NULL OR (
451
+ length(cost_usd_equivalent) > 0
452
+ AND cost_usd_equivalent NOT GLOB '*[^0-9.]*'
453
+ AND cost_usd_equivalent NOT LIKE '.%'
454
+ AND cost_usd_equivalent NOT LIKE '%.'
455
+ AND cost_usd_equivalent NOT LIKE '%.%.%'
456
+ AND (cost_usd_equivalent = '0' OR cost_usd_equivalent NOT GLOB '0[0-9]*')
457
+ )
458
+ ),
459
+ CHECK (
460
+ cost_currency IS NULL OR (
461
+ length(cost_currency) BETWEEN 3 AND 12
462
+ AND substr(cost_currency, 1, 1) GLOB '[A-Z]'
463
+ AND cost_currency NOT GLOB '*[^A-Z0-9]*'
464
+ )
465
+ ),
466
+ CHECK (
467
+ cost_kind IS NULL
468
+ OR (cost_kind = 'charged'
469
+ AND cost_amount IS NOT NULL AND cost_currency IS NOT NULL
470
+ AND cost_source IS NOT NULL AND length(cost_source) > 0
471
+ AND cost_reason IS NULL)
472
+ OR (cost_kind = 'estimated'
473
+ AND cost_amount IS NOT NULL AND cost_currency IS NOT NULL
474
+ AND cost_usd_equivalent IS NULL
475
+ AND cost_source IS NOT NULL AND length(cost_source) > 0
476
+ AND cost_reason IS NULL)
477
+ OR (cost_kind = 'unknown'
478
+ AND cost_amount IS NULL AND cost_currency IS NULL
479
+ AND cost_usd_equivalent IS NULL AND cost_source IS NULL
480
+ AND cost_reason IS NOT NULL AND length(cost_reason) > 0)
481
+ ),
482
+ UNIQUE (model_call_id, sequence),
483
+ FOREIGN KEY (model_call_id) REFERENCES model_calls(id) ON DELETE CASCADE
484
+ ) STRICT;
485
+
486
+ CREATE INDEX IF NOT EXISTS provider_requests_model_call_id
487
+ ON provider_requests (model_call_id, sequence);
488
+
489
+ CREATE TRIGGER IF NOT EXISTS provider_requests_identity_immutable
490
+ BEFORE UPDATE OF model_call_id, sequence, provider, model, started_at ON provider_requests
491
+ BEGIN
492
+ SELECT RAISE(ABORT, 'provider request identity is immutable');
493
+ END;
494
+
495
+ CREATE TRIGGER IF NOT EXISTS provider_requests_state_forward_only
496
+ BEFORE UPDATE OF state ON provider_requests
497
+ WHEN NOT (NEW.state = OLD.state OR (OLD.state = 'pending' AND NEW.state = 'settled'))
498
+ BEGIN
499
+ SELECT RAISE(ABORT, 'provider request may only settle once');
500
+ END;
501
+
502
+ CREATE TRIGGER IF NOT EXISTS provider_requests_settlement_immutable
503
+ BEFORE UPDATE OF outcome, status,
504
+ usage_input, usage_output, usage_total,
505
+ usage_input_no_cache, usage_input_cache_read, usage_input_cache_write,
506
+ usage_output_text, usage_output_reasoning,
507
+ cost_kind, cost_amount, cost_currency, cost_usd_equivalent,
508
+ cost_source, cost_reason, completed_at
509
+ ON provider_requests
510
+ WHEN OLD.state != 'pending'
394
511
  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;
512
+ SELECT RAISE(ABORT, 'provider request settlement is immutable');
403
513
  END;
404
514
 
405
515
  -- derivations
@@ -413,6 +523,7 @@ CREATE TABLE IF NOT EXISTS derivations (
413
523
  state TEXT NOT NULL DEFAULT 'building' CHECK (state IN ('building', 'complete')),
414
524
  disposition TEXT CHECK (disposition IN ('vector', 'lexical', 'excluded', 'nonsemantic', 'failed')),
415
525
  reason TEXT,
526
+ parse_issues INTEGER CHECK (parse_issues IS NULL OR parse_issues > 0),
416
527
  CHECK ((state = 'building' AND disposition IS NULL) OR (state = 'complete' AND disposition IS NOT NULL))
417
528
  ) STRICT;
418
529
 
@@ -524,14 +635,11 @@ BEGIN
524
635
  UPDATE entries SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = NEW.entry_id;
525
636
  END;
526
637
 
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);
638
+ CREATE TRIGGER IF NOT EXISTS entries_touch_on_channel_update
639
+ AFTER UPDATE ON entry_channels
640
+ BEGIN
641
+ UPDATE entries SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = NEW.entry_id;
642
+ END;
535
643
 
536
644
  -- symbol_defs
537
645
  -- @graph NODES ({§relation-indexed-dialects}). Code symbol definitions, populated
@@ -623,12 +731,15 @@ CREATE TABLE IF NOT EXISTS log_entries (
623
731
  ambient_event_id INTEGER REFERENCES ambient_events(id),
624
732
  -- Search derivation attached to this durable log result, when available.
625
733
  deep_hash TEXT REFERENCES derivations(deep_hash),
734
+ -- Exact logical provider call represented by a BARE result or model-emission
735
+ -- mirror. Other operation and ambient rows carry no model-call identity.
736
+ model_call_id INTEGER REFERENCES model_calls(id),
626
737
 
627
738
  -- 'error' is an ACTIONLESS row ({§operation-results} — errors are log items): a parse failure that
628
739
  -- produced no op still records a log entry (op='error', status_rx≥400, no target) so the model
629
740
  -- 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.
741
+ -- Actionless artifacts carry NULL here: kernel-authored worker initialization
742
+ -- ({§worker-initialization-entry}) or a model emission ({§model-entry}).
632
743
  -- No op enum here: the grammar op set is grammar's contract (PlurnkOp), and this column is written
633
744
  -- only by the PlurnkOp-typed engine (grammar ops), service row selectors, or NULL for no op.
634
745
  -- A SQL enum would be a hand-copy of grammar's op list that silently goes stale on every new verb
@@ -665,7 +776,9 @@ CREATE TABLE IF NOT EXISTS log_entries (
665
776
 
666
777
  expanded INTEGER NOT NULL DEFAULT 1 CHECK (expanded IN (0, 1)),
667
778
 
668
- CHECK ((op IS NULL) = COALESCE(json_extract(attrs, '$.kind') = 'model_emission', 0)),
779
+ CHECK ((op IS NULL) = COALESCE(json_extract(attrs, '$.kind') IN ('initialization', 'model_emission'), 0)),
780
+ CHECK (json_extract(attrs, '$.kind') != 'initialization' OR origin = 'plurnk'),
781
+ CHECK (json_extract(attrs, '$.kind') != 'model_emission' OR origin = 'model'),
669
782
 
670
783
  FOREIGN KEY (worker_id) REFERENCES workers(id) ON DELETE CASCADE,
671
784
  FOREIGN KEY (loop_id) REFERENCES loops(id) ON DELETE CASCADE,
@@ -676,37 +789,388 @@ CREATE UNIQUE INDEX IF NOT EXISTS log_entries_turn_id_sequence ON log_entries (t
676
789
  CREATE INDEX IF NOT EXISTS log_entries_worker_id ON log_entries (worker_id);
677
790
  CREATE INDEX IF NOT EXISTS log_entries_loop_id ON log_entries (loop_id);
678
791
  CREATE INDEX IF NOT EXISTS log_entries_at ON log_entries (at);
792
+ CREATE UNIQUE INDEX IF NOT EXISTS log_entries_model_call_id
793
+ ON log_entries (model_call_id)
794
+ WHERE model_call_id IS NOT NULL;
679
795
  CREATE UNIQUE INDEX IF NOT EXISTS log_entries_worker_ambient_event
680
796
  ON log_entries (worker_id, ambient_event_id)
681
797
  WHERE ambient_event_id IS NOT NULL;
682
798
 
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.
799
+ CREATE TRIGGER IF NOT EXISTS log_entries_model_call_valid
800
+ BEFORE INSERT ON log_entries
801
+ WHEN NEW.model_call_id IS NOT NULL
802
+ AND NOT EXISTS (
803
+ SELECT 1
804
+ FROM model_calls call
805
+ WHERE call.id = NEW.model_call_id
806
+ AND call.turn_id = NEW.turn_id
807
+ AND call.state != 'pending'
808
+ AND (
809
+ (call.kind = 'bare' AND NEW.op = 'BARE')
810
+ OR (
811
+ call.kind = 'emission'
812
+ AND NEW.op IS NULL
813
+ AND json_extract(NEW.attrs, '$.kind') = 'model_emission'
814
+ )
815
+ )
816
+ )
817
+ BEGIN
818
+ SELECT RAISE(ABORT, 'log entry model call does not match its represented result');
819
+ END;
820
+
821
+ -- {§log-item-tags} — log classification plus OPEN/FOLD selection and mutation.
822
+ -- CASCADE erases the classification with its row.
686
823
  CREATE TABLE IF NOT EXISTS log_tags (
687
824
  log_entry_id INTEGER NOT NULL,
688
- tag TEXT NOT NULL CHECK (length(tag) > 0),
825
+ tag TEXT NOT NULL,
689
826
  PRIMARY KEY (log_entry_id, tag),
690
827
  FOREIGN KEY (log_entry_id) REFERENCES log_entries(id) ON DELETE CASCADE
691
828
  ) STRICT, WITHOUT ROWID;
692
829
 
830
+ -- The relational table owns stored tag identity. Producers and curation
831
+ -- signals carry signs, but the stored name never does; delimiters, whitespace,
832
+ -- and control characters cannot become durable classifications through a
833
+ -- private SQL caller either.
834
+ CREATE TRIGGER IF NOT EXISTS log_tags_name_valid
835
+ BEFORE INSERT ON log_tags
836
+ WHEN length(NEW.tag) = 0
837
+ OR substr(NEW.tag, 1, 1) IN ('+', '-')
838
+ OR instr(NEW.tag, '[') > 0
839
+ OR instr(NEW.tag, ']') > 0
840
+ OR instr(NEW.tag, ',') > 0
841
+ OR instr(NEW.tag, char(0)) > 0
842
+ OR EXISTS (
843
+ WITH RECURSIVE offsets(position) AS (
844
+ SELECT 1
845
+ UNION ALL
846
+ SELECT position + 1 FROM offsets WHERE position < length(NEW.tag)
847
+ )
848
+ SELECT 1
849
+ FROM offsets
850
+ WHERE unicode(substr(NEW.tag, position, 1)) BETWEEN 0 AND 32
851
+ OR unicode(substr(NEW.tag, position, 1)) BETWEEN 127 AND 159
852
+ OR unicode(substr(NEW.tag, position, 1)) IN (160, 5760, 8232, 8233, 8239, 8287, 12288, 65279)
853
+ OR unicode(substr(NEW.tag, position, 1)) BETWEEN 8192 AND 8202
854
+ )
855
+ BEGIN
856
+ SELECT RAISE(ABORT, 'log tag name is invalid');
857
+ END;
858
+
693
859
  CREATE INDEX IF NOT EXISTS log_tags_tag ON log_tags (tag);
694
860
 
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.
861
+ -- Signal-derived classification is part of the log-row insert, not a later JS
862
+ -- write. This keeps the operation, its ambient occurrence, and its complete
863
+ -- initial folksonomy inside one SQLite commit.
864
+ CREATE TRIGGER IF NOT EXISTS log_entries_classify_signal
865
+ AFTER INSERT ON log_entries
866
+ WHEN NEW.op IN ('FIND', 'READ', 'EDIT', 'COPY', 'MOVE', 'BARE')
867
+ AND NEW.signal IS NOT NULL
868
+ BEGIN
869
+ SELECT CASE
870
+ WHEN json_type(NEW.signal) != 'array'
871
+ OR EXISTS (
872
+ SELECT 1 FROM json_each(NEW.signal)
873
+ WHERE type != 'text'
874
+ OR length(value) = 0
875
+ OR substr(value, 1, 1) = '-'
876
+ OR (
877
+ substr(value, 1, 1) = '+'
878
+ AND (length(value) < 2 OR substr(value, 2, 1) IN ('+', '-'))
879
+ )
880
+ OR instr(value, '[') > 0
881
+ OR instr(value, ']') > 0
882
+ OR instr(value, ',') > 0
883
+ OR instr(value, char(0)) > 0
884
+ OR EXISTS (
885
+ WITH RECURSIVE offsets(position) AS (
886
+ SELECT 1
887
+ UNION ALL
888
+ SELECT position + 1 FROM offsets WHERE position < length(value)
889
+ )
890
+ SELECT 1
891
+ FROM offsets
892
+ WHERE unicode(substr(value, position, 1)) BETWEEN 0 AND 32
893
+ OR unicode(substr(value, position, 1)) BETWEEN 127 AND 159
894
+ OR unicode(substr(value, position, 1)) IN (160, 5760, 8232, 8233, 8239, 8287, 12288, 65279)
895
+ OR unicode(substr(value, position, 1)) BETWEEN 8192 AND 8202
896
+ )
897
+ )
898
+ THEN RAISE(ABORT, 'classifying log operation signal accepts only tag or +tag additions')
899
+ END;
900
+ INSERT OR IGNORE INTO log_tags (log_entry_id, tag)
901
+ SELECT NEW.id, CASE WHEN substr(value, 1, 1) = '+' THEN substr(value, 2) ELSE value END
902
+ FROM json_each(NEW.signal);
903
+
904
+ -- The ambient-event and classification AFTER INSERT triggers may run in
905
+ -- either order. If the event already exists, finish its initial
906
+ -- snapshot here; otherwise its own trigger aggregates these rows later.
907
+ UPDATE ambient_events
908
+ SET tags = COALESCE((
909
+ SELECT json_group_array(tag)
910
+ FROM (SELECT tag FROM log_tags WHERE log_entry_id = NEW.id ORDER BY tag)
911
+ ), '[]')
912
+ WHERE kind = 'edit'
913
+ AND producer_worker_id = NEW.worker_id
914
+ AND source_record_id = NEW.id;
915
+ END;
916
+
917
+ -- Successful OPEN/FOLD rows are durable curation events even though their
918
+ -- ordinary packet projection is suppressed ({§fold-open-meta-operations}).
919
+ -- Preserve the exact selected set and each target's pre-event visibility so a
920
+ -- broad selector never collapses into the lossy fact `matched: N`.
921
+ CREATE TABLE IF NOT EXISTS log_curation_effects (
922
+ operation_log_entry_id INTEGER NOT NULL,
923
+ target_log_entry_id INTEGER NOT NULL,
924
+ expanded_before INTEGER NOT NULL CHECK (expanded_before IN (0, 1)),
925
+ tags_added TEXT NOT NULL DEFAULT '[]'
926
+ CHECK (json_valid(tags_added) AND json_type(tags_added) = 'array'),
927
+ tags_removed TEXT NOT NULL DEFAULT '[]'
928
+ CHECK (json_valid(tags_removed) AND json_type(tags_removed) = 'array'),
929
+ PRIMARY KEY (operation_log_entry_id, target_log_entry_id),
930
+ CHECK (operation_log_entry_id != target_log_entry_id),
931
+ FOREIGN KEY (operation_log_entry_id) REFERENCES log_entries(id) ON DELETE CASCADE,
932
+ FOREIGN KEY (target_log_entry_id) REFERENCES log_entries(id) ON DELETE CASCADE
933
+ ) STRICT, WITHOUT ROWID;
934
+
935
+ CREATE INDEX IF NOT EXISTS log_curation_effects_target
936
+ ON log_curation_effects (target_log_entry_id);
937
+
938
+ -- Make an invalid curation record structurally unavailable: the event must be
939
+ -- one successful OPEN/FOLD row, both rows must belong to the same worker, and
940
+ -- both exact delta sets must contain only unique canonical tag identities.
941
+ CREATE TRIGGER IF NOT EXISTS log_curation_effects_valid
942
+ BEFORE INSERT ON log_curation_effects
943
+ BEGIN
944
+ SELECT CASE WHEN
945
+ NOT EXISTS (
946
+ SELECT 1
947
+ FROM log_entries operation
948
+ JOIN log_entries target ON target.id = NEW.target_log_entry_id
949
+ WHERE operation.id = NEW.operation_log_entry_id
950
+ AND operation.op IN ('OPEN', 'FOLD')
951
+ AND operation.status_rx < 400
952
+ AND operation.worker_id = target.worker_id
953
+ )
954
+ OR EXISTS (
955
+ SELECT 1
956
+ FROM (
957
+ SELECT value, type FROM json_each(NEW.tags_added)
958
+ UNION ALL
959
+ SELECT value, type FROM json_each(NEW.tags_removed)
960
+ ) tag
961
+ WHERE tag.type != 'text'
962
+ OR length(tag.value) = 0
963
+ OR substr(tag.value, 1, 1) IN ('+', '-')
964
+ OR instr(tag.value, '[') > 0
965
+ OR instr(tag.value, ']') > 0
966
+ OR instr(tag.value, ',') > 0
967
+ OR instr(tag.value, char(0)) > 0
968
+ OR EXISTS (
969
+ WITH RECURSIVE offsets(position) AS (
970
+ SELECT 1
971
+ UNION ALL
972
+ SELECT position + 1 FROM offsets WHERE position < length(tag.value)
973
+ )
974
+ SELECT 1
975
+ FROM offsets
976
+ WHERE unicode(substr(tag.value, position, 1)) BETWEEN 0 AND 32
977
+ OR unicode(substr(tag.value, position, 1)) BETWEEN 127 AND 159
978
+ OR unicode(substr(tag.value, position, 1)) IN (160, 5760, 8232, 8233, 8239, 8287, 12288, 65279)
979
+ OR unicode(substr(tag.value, position, 1)) BETWEEN 8192 AND 8202
980
+ )
981
+ )
982
+ OR (SELECT COUNT(*) FROM json_each(NEW.tags_added))
983
+ != (SELECT COUNT(DISTINCT value) FROM json_each(NEW.tags_added))
984
+ OR (SELECT COUNT(*) FROM json_each(NEW.tags_removed))
985
+ != (SELECT COUNT(DISTINCT value) FROM json_each(NEW.tags_removed))
986
+ OR EXISTS (
987
+ SELECT 1
988
+ FROM json_each(NEW.tags_added) addition
989
+ JOIN json_each(NEW.tags_removed) removal ON removal.value = addition.value
990
+ )
991
+ THEN RAISE(ABORT, 'invalid log curation effect') END;
992
+ END;
993
+
994
+ -- The dispatcher binds an exact, transient curation plan into attrs on the
995
+ -- successful OPEN/FOLD row. No other row may carry that private payload.
996
+ CREATE TRIGGER IF NOT EXISTS log_entries_curation_payload_valid
997
+ BEFORE INSERT ON log_entries
998
+ WHEN json_type(NEW.attrs, '$.__plurnk_curation') IS NOT NULL
999
+ AND NOT (
1000
+ NEW.op IN ('OPEN', 'FOLD')
1001
+ AND NEW.status_rx < 400
1002
+ AND json_type(NEW.attrs, '$.__plurnk_curation') = 'object'
1003
+ )
1004
+ BEGIN
1005
+ SELECT RAISE(ABORT, 'private log curation payload requires a successful OPEN/FOLD row');
1006
+ END;
1007
+
1008
+ -- One outer INSERT owns the whole landed curation event: exact selected rows,
1009
+ -- their pre-event visibility and tag deltas, the requested visibility, and the
1010
+ -- resulting classifications. Trigger failure rolls the operation row and all
1011
+ -- effects back together. The private plan is erased before INSERT returns.
1012
+ CREATE TRIGGER IF NOT EXISTS log_entries_apply_curation
1013
+ AFTER INSERT ON log_entries
1014
+ WHEN NEW.op IN ('OPEN', 'FOLD')
1015
+ AND NEW.status_rx < 400
1016
+ AND json_type(NEW.attrs, '$.__plurnk_curation') = 'object'
1017
+ BEGIN
1018
+ SELECT CASE WHEN
1019
+ COALESCE(json_type(NEW.attrs, '$.__plurnk_curation.ids'), '') != 'array'
1020
+ OR COALESCE(json_type(NEW.attrs, '$.__plurnk_curation.add'), '') != 'array'
1021
+ OR COALESCE(json_type(NEW.attrs, '$.__plurnk_curation.remove'), '') != 'array'
1022
+ OR COALESCE(json_type(NEW.attrs, '$.__plurnk_curation.expanded'), '') != 'integer'
1023
+ OR json_extract(NEW.attrs, '$.__plurnk_curation.expanded') NOT IN (0, 1)
1024
+ OR json_array_length(NEW.attrs, '$.__plurnk_curation.ids') = 0
1025
+ OR EXISTS (
1026
+ SELECT 1 FROM json_each(NEW.attrs, '$.__plurnk_curation.ids')
1027
+ WHERE type != 'integer' OR value <= 0
1028
+ )
1029
+ OR (
1030
+ SELECT COUNT(*) FROM json_each(NEW.attrs, '$.__plurnk_curation.ids')
1031
+ ) != (
1032
+ SELECT COUNT(DISTINCT value) FROM json_each(NEW.attrs, '$.__plurnk_curation.ids')
1033
+ )
1034
+ OR EXISTS (
1035
+ SELECT 1
1036
+ FROM json_each(NEW.attrs, '$.__plurnk_curation.ids') selected
1037
+ LEFT JOIN log_entries target ON target.id = selected.value
1038
+ WHERE target.id IS NULL OR target.worker_id != NEW.worker_id OR target.id = NEW.id
1039
+ )
1040
+ OR EXISTS (
1041
+ SELECT 1
1042
+ FROM (
1043
+ SELECT value, type FROM json_each(NEW.attrs, '$.__plurnk_curation.add')
1044
+ UNION ALL
1045
+ SELECT value, type FROM json_each(NEW.attrs, '$.__plurnk_curation.remove')
1046
+ ) tag
1047
+ WHERE tag.type != 'text'
1048
+ OR length(tag.value) = 0
1049
+ OR substr(tag.value, 1, 1) IN ('+', '-')
1050
+ OR instr(tag.value, '[') > 0
1051
+ OR instr(tag.value, ']') > 0
1052
+ OR instr(tag.value, ',') > 0
1053
+ OR instr(tag.value, char(0)) > 0
1054
+ OR EXISTS (
1055
+ WITH RECURSIVE offsets(position) AS (
1056
+ SELECT 1
1057
+ UNION ALL
1058
+ SELECT position + 1 FROM offsets WHERE position < length(tag.value)
1059
+ )
1060
+ SELECT 1
1061
+ FROM offsets
1062
+ WHERE unicode(substr(tag.value, position, 1)) BETWEEN 0 AND 32
1063
+ OR unicode(substr(tag.value, position, 1)) BETWEEN 127 AND 159
1064
+ OR unicode(substr(tag.value, position, 1)) IN (160, 5760, 8232, 8233, 8239, 8287, 12288, 65279)
1065
+ OR unicode(substr(tag.value, position, 1)) BETWEEN 8192 AND 8202
1066
+ )
1067
+ )
1068
+ OR (
1069
+ SELECT COUNT(*) FROM json_each(NEW.attrs, '$.__plurnk_curation.add')
1070
+ ) != (
1071
+ SELECT COUNT(DISTINCT value) FROM json_each(NEW.attrs, '$.__plurnk_curation.add')
1072
+ )
1073
+ OR (
1074
+ SELECT COUNT(*) FROM json_each(NEW.attrs, '$.__plurnk_curation.remove')
1075
+ ) != (
1076
+ SELECT COUNT(DISTINCT value) FROM json_each(NEW.attrs, '$.__plurnk_curation.remove')
1077
+ )
1078
+ OR EXISTS (
1079
+ SELECT 1
1080
+ FROM json_each(NEW.attrs, '$.__plurnk_curation.add') addition
1081
+ JOIN json_each(NEW.attrs, '$.__plurnk_curation.remove') removal
1082
+ ON removal.value = addition.value
1083
+ )
1084
+ THEN RAISE(ABORT, 'invalid private log curation payload') END;
1085
+
1086
+ INSERT INTO log_curation_effects (
1087
+ operation_log_entry_id,
1088
+ target_log_entry_id,
1089
+ expanded_before,
1090
+ tags_added,
1091
+ tags_removed
1092
+ )
1093
+ SELECT
1094
+ NEW.id,
1095
+ target.id,
1096
+ target.expanded,
1097
+ COALESCE((
1098
+ SELECT json_group_array(ordered.tag)
1099
+ FROM (
1100
+ SELECT addition.value AS tag
1101
+ FROM json_each(NEW.attrs, '$.__plurnk_curation.add') addition
1102
+ WHERE NOT EXISTS (
1103
+ SELECT 1 FROM log_tags
1104
+ WHERE log_entry_id = target.id AND tag = addition.value
1105
+ )
1106
+ ORDER BY addition.value
1107
+ ) ordered
1108
+ ), '[]'),
1109
+ COALESCE((
1110
+ SELECT json_group_array(ordered.tag)
1111
+ FROM (
1112
+ SELECT removal.value AS tag
1113
+ FROM json_each(NEW.attrs, '$.__plurnk_curation.remove') removal
1114
+ WHERE EXISTS (
1115
+ SELECT 1 FROM log_tags
1116
+ WHERE log_entry_id = target.id AND tag = removal.value
1117
+ )
1118
+ ORDER BY removal.value
1119
+ ) ordered
1120
+ ), '[]')
1121
+ FROM json_each(NEW.attrs, '$.__plurnk_curation.ids') selected
1122
+ JOIN log_entries target ON target.id = selected.value;
1123
+
1124
+ UPDATE log_entries
1125
+ SET expanded = json_extract(NEW.attrs, '$.__plurnk_curation.expanded')
1126
+ WHERE id IN (
1127
+ SELECT value FROM json_each(NEW.attrs, '$.__plurnk_curation.ids')
1128
+ );
1129
+
1130
+ DELETE FROM log_tags
1131
+ WHERE log_entry_id IN (
1132
+ SELECT value FROM json_each(NEW.attrs, '$.__plurnk_curation.ids')
1133
+ )
1134
+ AND tag IN (
1135
+ SELECT value FROM json_each(NEW.attrs, '$.__plurnk_curation.remove')
1136
+ );
1137
+
1138
+ INSERT OR IGNORE INTO log_tags (log_entry_id, tag)
1139
+ SELECT selected.value, addition.value
1140
+ FROM json_each(NEW.attrs, '$.__plurnk_curation.ids') selected
1141
+ CROSS JOIN json_each(NEW.attrs, '$.__plurnk_curation.add') addition;
1142
+
1143
+ UPDATE log_entries
1144
+ SET attrs = json_remove(attrs, '$.__plurnk_curation')
1145
+ WHERE id = NEW.id;
1146
+ END;
1147
+
1148
+ -- Column-scoped immutability: the original action's identity and target never
1149
+ -- change; the proposal lifecycle is allowed to mutate state, outcome,
1150
+ -- status_rx, rx, expanded. Keep attrs separate so the curation trigger's one
1151
+ -- private-payload removal cannot exempt changes to any other core column.
698
1152
  CREATE TRIGGER IF NOT EXISTS log_entries_immutable_core
699
1153
  BEFORE UPDATE OF
700
- worker_id, loop_id, turn_id, sequence, at, origin, source,
1154
+ worker_id, loop_id, turn_id, sequence, at, origin, source, model_call_id,
701
1155
  op, suffix, signal,
702
1156
  scheme, username, password, hostname,
703
1157
  port, pathname, query, fragment,
704
- lineMarker, tx, mimetype_tx, mimetype_rx, attrs
1158
+ lineMarker, tx, mimetype_tx, mimetype_rx
705
1159
  ON log_entries
706
1160
  BEGIN
707
1161
  SELECT RAISE(ABORT, 'log_entries core fields are immutable; only state/outcome/status_rx/rx/expanded may change');
708
1162
  END;
709
1163
 
1164
+ CREATE TRIGGER IF NOT EXISTS log_entries_immutable_attrs
1165
+ BEFORE UPDATE OF attrs ON log_entries
1166
+ WHEN COALESCE((
1167
+ json_type(OLD.attrs, '$.__plurnk_curation') = 'object'
1168
+ AND NEW.attrs = json_remove(OLD.attrs, '$.__plurnk_curation')
1169
+ ), 0) = 0
1170
+ BEGIN
1171
+ SELECT RAISE(ABORT, 'log_entries attrs are immutable outside curation payload removal');
1172
+ END;
1173
+
710
1174
  -- The engine may stamp an originating row exactly once. No later reassignment
711
1175
  -- can sever or counterfeit the occurrence identity.
712
1176
  CREATE TRIGGER IF NOT EXISTS log_entries_ambient_event_once
@@ -734,10 +1198,17 @@ WHEN NEW.ambient_event_id IS NULL
734
1198
  BEGIN
735
1199
  INSERT INTO ambient_events (
736
1200
  workspace_id, producer_worker_id, kind, source_record_id, source,
737
- op, scheme, hostname, pathname, rx, attrs, status_rx
1201
+ op, scheme, hostname, pathname, rx, attrs, tags, status_rx
738
1202
  )
739
1203
  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
1204
+ 'EDIT', NEW.scheme, NEW.hostname, NEW.pathname, NEW.rx, NEW.attrs,
1205
+ COALESCE((
1206
+ SELECT json_group_array(ordered.tag)
1207
+ FROM (
1208
+ SELECT tag FROM log_tags WHERE log_entry_id = NEW.id ORDER BY tag
1209
+ ) ordered
1210
+ ), '[]'),
1211
+ NEW.status_rx
741
1212
  FROM workers w
742
1213
  WHERE w.id = NEW.worker_id;
743
1214
  UPDATE log_entries SET ambient_event_id = last_insert_rowid() WHERE id = NEW.id;
@@ -761,97 +1232,22 @@ WHEN OLD.state = 'proposed'
761
1232
  BEGIN
762
1233
  INSERT INTO ambient_events (
763
1234
  workspace_id, producer_worker_id, kind, source_record_id, source,
764
- op, scheme, hostname, pathname, rx, attrs, status_rx
1235
+ op, scheme, hostname, pathname, rx, attrs, tags, status_rx
765
1236
  )
766
1237
  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
1238
+ 'EDIT', NEW.scheme, NEW.hostname, NEW.pathname, NEW.rx, NEW.attrs,
1239
+ COALESCE((
1240
+ SELECT json_group_array(ordered.tag)
1241
+ FROM (
1242
+ SELECT tag FROM log_tags WHERE log_entry_id = NEW.id ORDER BY tag
1243
+ ) ordered
1244
+ ), '[]'),
1245
+ NEW.status_rx
768
1246
  FROM workers w
769
1247
  WHERE w.id = NEW.worker_id;
770
1248
  UPDATE log_entries SET ambient_event_id = last_insert_rowid() WHERE id = NEW.id;
771
1249
  END;
772
1250
 
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
1251
  -- subscriptions
856
1252
  -- Durable subscription lifecycle per SPEC {§subscriptions}. The row records what
857
1253
  -- the worker holds and routes cancellation to a separate process-local callable;