@plurnk/plurnk-service 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/.env.defaults +20 -0
  2. package/INSTALL.md +14 -16
  3. package/README.md +1 -1
  4. package/SPEC.md +635 -296
  5. package/digest-sql/channel-dispositions/dispositions.sql +2 -2
  6. package/digest-sql/entry-dispositions/dispositions.sql +2 -2
  7. package/dist/Paths.d.ts +1 -2
  8. package/dist/Paths.d.ts.map +1 -1
  9. package/dist/Paths.js +1 -9
  10. package/dist/Paths.js.map +1 -1
  11. package/dist/build-info.json +1 -1
  12. package/dist/core/ChannelWrite.d.ts +8 -3
  13. package/dist/core/ChannelWrite.d.ts.map +1 -1
  14. package/dist/core/ChannelWrite.js +40 -8
  15. package/dist/core/ChannelWrite.js.map +1 -1
  16. package/dist/core/ChannelWrite.sql +17 -4
  17. package/dist/core/CoreSchemeServices.d.ts +7 -3
  18. package/dist/core/CoreSchemeServices.d.ts.map +1 -1
  19. package/dist/core/CoreSchemeServices.js +8 -1
  20. package/dist/core/CoreSchemeServices.js.map +1 -1
  21. package/dist/core/Dispatcher.d.ts +9 -1
  22. package/dist/core/Dispatcher.d.ts.map +1 -1
  23. package/dist/core/Dispatcher.js +321 -221
  24. package/dist/core/Dispatcher.js.map +1 -1
  25. package/dist/core/Engine.d.ts +13 -4
  26. package/dist/core/Engine.d.ts.map +1 -1
  27. package/dist/core/Engine.js +44 -22
  28. package/dist/core/Engine.js.map +1 -1
  29. package/dist/core/Engine.sql +101 -81
  30. package/dist/core/EntryAddressBinding.d.ts +32 -0
  31. package/dist/core/EntryAddressBinding.d.ts.map +1 -0
  32. package/dist/core/EntryAddressBinding.js +93 -0
  33. package/dist/core/EntryAddressBinding.js.map +1 -0
  34. package/dist/core/ExecutorRegistry.d.ts +4 -4
  35. package/dist/core/ExecutorRegistry.d.ts.map +1 -1
  36. package/dist/core/ExecutorRegistry.js +28 -28
  37. package/dist/core/ExecutorRegistry.js.map +1 -1
  38. package/dist/core/LogBody.d.ts.map +1 -1
  39. package/dist/core/LogBody.js +19 -1
  40. package/dist/core/LogBody.js.map +1 -1
  41. package/dist/core/OperatorConfig.d.ts.map +1 -1
  42. package/dist/core/OperatorConfig.js +5 -2
  43. package/dist/core/OperatorConfig.js.map +1 -1
  44. package/dist/core/OverflowTurn.d.ts.map +1 -1
  45. package/dist/core/OverflowTurn.js +5 -1
  46. package/dist/core/OverflowTurn.js.map +1 -1
  47. package/dist/core/Owner.js +1 -1
  48. package/dist/core/Owner.js.map +1 -1
  49. package/dist/core/PacketBuilder.d.ts +5 -1
  50. package/dist/core/PacketBuilder.d.ts.map +1 -1
  51. package/dist/core/PacketBuilder.js +21 -11
  52. package/dist/core/PacketBuilder.js.map +1 -1
  53. package/dist/core/ProposalLifecycle.d.ts +4 -1
  54. package/dist/core/ProposalLifecycle.d.ts.map +1 -1
  55. package/dist/core/ProposalLifecycle.js +85 -26
  56. package/dist/core/ProposalLifecycle.js.map +1 -1
  57. package/dist/core/ReasoningEvent.d.ts +13 -0
  58. package/dist/core/ReasoningEvent.d.ts.map +1 -0
  59. package/dist/core/ReasoningEvent.js +6 -0
  60. package/dist/core/ReasoningEvent.js.map +1 -0
  61. package/dist/core/ResourceMutations.d.ts +15 -19
  62. package/dist/core/ResourceMutations.d.ts.map +1 -1
  63. package/dist/core/ResourceMutations.js +113 -38
  64. package/dist/core/ResourceMutations.js.map +1 -1
  65. package/dist/core/SchemeRegistry.d.ts +12 -11
  66. package/dist/core/SchemeRegistry.d.ts.map +1 -1
  67. package/dist/core/SchemeRegistry.js +52 -47
  68. package/dist/core/SchemeRegistry.js.map +1 -1
  69. package/dist/core/ToolResources.d.ts.map +1 -1
  70. package/dist/core/ToolResources.js +20 -19
  71. package/dist/core/ToolResources.js.map +1 -1
  72. package/dist/core/Turn.d.ts +1 -1
  73. package/dist/core/Turn.d.ts.map +1 -1
  74. package/dist/core/TurnOps.d.ts +2 -2
  75. package/dist/core/TurnOps.d.ts.map +1 -1
  76. package/dist/core/TurnOps.js +14 -4
  77. package/dist/core/TurnOps.js.map +1 -1
  78. package/dist/core/TurnRunner.d.ts +46 -3
  79. package/dist/core/TurnRunner.d.ts.map +1 -1
  80. package/dist/core/TurnRunner.js +165 -78
  81. package/dist/core/TurnRunner.js.map +1 -1
  82. package/dist/core/WorkerName.d.ts +1 -0
  83. package/dist/core/WorkerName.d.ts.map +1 -1
  84. package/dist/core/WorkerName.js +2 -1
  85. package/dist/core/WorkerName.js.map +1 -1
  86. package/dist/core/WorkerName.sql +17 -2
  87. package/dist/core/WorkspaceGate.js +1 -1
  88. package/dist/core/WorkspaceGate.js.map +1 -1
  89. package/dist/core/caps/CapsResolve.d.ts +2 -2
  90. package/dist/core/caps/CapsResolve.d.ts.map +1 -1
  91. package/dist/core/caps/CapsResolve.js +5 -7
  92. package/dist/core/caps/CapsResolve.js.map +1 -1
  93. package/dist/core/caps/DbChannelCaps.d.ts +1 -1
  94. package/dist/core/caps/DbChannelCaps.d.ts.map +1 -1
  95. package/dist/core/caps/DbChannelCaps.js +13 -11
  96. package/dist/core/caps/DbChannelCaps.js.map +1 -1
  97. package/dist/core/caps/DbEntryCaps.d.ts +5 -5
  98. package/dist/core/caps/DbEntryCaps.d.ts.map +1 -1
  99. package/dist/core/caps/DbEntryCaps.js +27 -30
  100. package/dist/core/caps/DbEntryCaps.js.map +1 -1
  101. package/dist/core/caps/DbNotifyCaps.d.ts +1 -1
  102. package/dist/core/caps/DbNotifyCaps.d.ts.map +1 -1
  103. package/dist/core/caps/DbNotifyCaps.js +5 -3
  104. package/dist/core/caps/DbNotifyCaps.js.map +1 -1
  105. package/dist/core/caps/DbSubscriptionCaps.d.ts +3 -3
  106. package/dist/core/caps/DbSubscriptionCaps.d.ts.map +1 -1
  107. package/dist/core/caps/DbSubscriptionCaps.js +16 -26
  108. package/dist/core/caps/DbSubscriptionCaps.js.map +1 -1
  109. package/dist/core/caps/SchemeCtxImpl.d.ts +6 -4
  110. package/dist/core/caps/SchemeCtxImpl.d.ts.map +1 -1
  111. package/dist/core/caps/SchemeCtxImpl.js +37 -5
  112. package/dist/core/caps/SchemeCtxImpl.js.map +1 -1
  113. package/dist/core/file-creation-policy.d.ts +9 -0
  114. package/dist/core/file-creation-policy.d.ts.map +1 -0
  115. package/dist/core/file-creation-policy.js +25 -0
  116. package/dist/core/file-creation-policy.js.map +1 -0
  117. package/dist/core/fork.d.ts +4 -1
  118. package/dist/core/fork.d.ts.map +1 -1
  119. package/dist/core/fork.js +27 -15
  120. package/dist/core/fork.js.map +1 -1
  121. package/dist/core/fork.sql +35 -22
  122. package/dist/core/git-membership.d.ts +19 -1
  123. package/dist/core/git-membership.d.ts.map +1 -1
  124. package/dist/core/git-membership.js +225 -50
  125. package/dist/core/git-membership.js.map +1 -1
  126. package/dist/core/packet-wire.d.ts.map +1 -1
  127. package/dist/core/packet-wire.js +28 -19
  128. package/dist/core/packet-wire.js.map +1 -1
  129. package/dist/core/plurnk-uri.d.ts +13 -2
  130. package/dist/core/plurnk-uri.d.ts.map +1 -1
  131. package/dist/core/plurnk-uri.js +59 -21
  132. package/dist/core/plurnk-uri.js.map +1 -1
  133. package/dist/core/scheme-types.d.ts +1 -0
  134. package/dist/core/scheme-types.d.ts.map +1 -1
  135. package/dist/core/worker-ops.sql +10 -6
  136. package/dist/core/worker-settings.d.ts +1 -0
  137. package/dist/core/worker-settings.d.ts.map +1 -1
  138. package/dist/core/worker-settings.js +7 -0
  139. package/dist/core/worker-settings.js.map +1 -1
  140. package/dist/core/workspace-settings.d.ts +2 -0
  141. package/dist/core/workspace-settings.d.ts.map +1 -1
  142. package/dist/core/workspace-settings.js +5 -1
  143. package/dist/core/workspace-settings.js.map +1 -1
  144. package/dist/digest/Digest.d.ts.map +1 -1
  145. package/dist/digest/Digest.js +1 -1
  146. package/dist/digest/Digest.js.map +1 -1
  147. package/dist/digest/digest.sql +1 -1
  148. package/dist/schemes/Exec.d.ts +3 -2
  149. package/dist/schemes/Exec.d.ts.map +1 -1
  150. package/dist/schemes/Exec.js +55 -22
  151. package/dist/schemes/Exec.js.map +1 -1
  152. package/dist/schemes/ExecOutputScheme.d.ts.map +1 -1
  153. package/dist/schemes/ExecOutputScheme.js +5 -4
  154. package/dist/schemes/ExecOutputScheme.js.map +1 -1
  155. package/dist/schemes/File.d.ts.map +1 -1
  156. package/dist/schemes/File.js +170 -80
  157. package/dist/schemes/File.js.map +1 -1
  158. package/dist/schemes/Prompt.d.ts +2 -3
  159. package/dist/schemes/Prompt.d.ts.map +1 -1
  160. package/dist/schemes/Prompt.js +4 -7
  161. package/dist/schemes/Prompt.js.map +1 -1
  162. package/dist/schemes/Skill.d.ts.map +1 -1
  163. package/dist/schemes/Skill.js +3 -1
  164. package/dist/schemes/Skill.js.map +1 -1
  165. package/dist/schemes/Worker.d.ts.map +1 -1
  166. package/dist/schemes/Worker.js +95 -49
  167. package/dist/schemes/Worker.js.map +1 -1
  168. package/dist/schemes/_entry-crud.d.ts +5 -5
  169. package/dist/schemes/_entry-crud.d.ts.map +1 -1
  170. package/dist/schemes/_entry-crud.js +22 -17
  171. package/dist/schemes/_entry-crud.js.map +1 -1
  172. package/dist/schemes/_entry-crud.sql +58 -23
  173. package/dist/schemes/_entry-find.d.ts +3 -2
  174. package/dist/schemes/_entry-find.d.ts.map +1 -1
  175. package/dist/schemes/_entry-find.js +26 -25
  176. package/dist/schemes/_entry-find.js.map +1 -1
  177. package/dist/schemes/_entry-find.sql +10 -7
  178. package/dist/schemes/_entry-manifest.d.ts +2 -2
  179. package/dist/schemes/_entry-manifest.d.ts.map +1 -1
  180. package/dist/schemes/_entry-manifest.js +6 -5
  181. package/dist/schemes/_entry-manifest.js.map +1 -1
  182. package/dist/schemes/_entry-ops.d.ts +6 -5
  183. package/dist/schemes/_entry-ops.d.ts.map +1 -1
  184. package/dist/schemes/_entry-ops.js +39 -46
  185. package/dist/schemes/_entry-ops.js.map +1 -1
  186. package/dist/schemes/_entry-ops.sql +9 -6
  187. package/dist/schemes/_entry-send.d.ts +2 -2
  188. package/dist/schemes/_entry-send.d.ts.map +1 -1
  189. package/dist/schemes/_entry-send.js +16 -14
  190. package/dist/schemes/_entry-send.js.map +1 -1
  191. package/dist/schemes/_search-index.d.ts.map +1 -1
  192. package/dist/schemes/_search-index.js +2 -0
  193. package/dist/schemes/_search-index.js.map +1 -1
  194. package/dist/server/Daemon.d.ts +42 -10
  195. package/dist/server/Daemon.d.ts.map +1 -1
  196. package/dist/server/Daemon.js +326 -248
  197. package/dist/server/Daemon.js.map +1 -1
  198. package/dist/server/DaemonModule.d.ts +84 -19
  199. package/dist/server/DaemonModule.d.ts.map +1 -1
  200. package/dist/server/DrainSupervisor.d.ts +3 -1
  201. package/dist/server/DrainSupervisor.d.ts.map +1 -1
  202. package/dist/server/DrainSupervisor.js +11 -9
  203. package/dist/server/DrainSupervisor.js.map +1 -1
  204. package/dist/server/Functionality.d.ts +44 -0
  205. package/dist/server/Functionality.d.ts.map +1 -0
  206. package/dist/server/Functionality.js +422 -0
  207. package/dist/server/Functionality.js.map +1 -0
  208. package/dist/server/FunctionalityManager.d.ts +22 -0
  209. package/dist/server/FunctionalityManager.d.ts.map +1 -0
  210. package/dist/server/FunctionalityManager.js +121 -0
  211. package/dist/server/FunctionalityManager.js.map +1 -0
  212. package/dist/server/SkillsFunctionality.d.ts +61 -0
  213. package/dist/server/SkillsFunctionality.d.ts.map +1 -0
  214. package/dist/server/SkillsFunctionality.js +449 -0
  215. package/dist/server/SkillsFunctionality.js.map +1 -0
  216. package/dist/server/WorkerCapabilities.d.ts +21 -0
  217. package/dist/server/WorkerCapabilities.d.ts.map +1 -0
  218. package/dist/server/WorkerCapabilities.js +214 -0
  219. package/dist/server/WorkerCapabilities.js.map +1 -0
  220. package/dist/server/WorkerResidency.d.ts +40 -0
  221. package/dist/server/WorkerResidency.d.ts.map +1 -0
  222. package/dist/server/WorkerResidency.js +283 -0
  223. package/dist/server/WorkerResidency.js.map +1 -0
  224. package/dist/server/client-input.d.ts +1 -0
  225. package/dist/server/client-input.d.ts.map +1 -1
  226. package/dist/server/client-input.js +22 -1
  227. package/dist/server/client-input.js.map +1 -1
  228. package/dist/server/dispatch-as-plurnk.d.ts +2 -2
  229. package/dist/server/dispatch-as-plurnk.d.ts.map +1 -1
  230. package/dist/server/dispatch-as-plurnk.js +89 -25
  231. package/dist/server/dispatch-as-plurnk.js.map +1 -1
  232. package/dist/server/drain.sql +11 -6
  233. package/dist/server/envelope.d.ts +6 -1
  234. package/dist/server/envelope.d.ts.map +1 -1
  235. package/dist/server/envelope.js +8 -2
  236. package/dist/server/envelope.js.map +1 -1
  237. package/dist/server/envelope.sql +11 -3
  238. package/dist/server/lifecycle-recovery.sql +13 -16
  239. package/dist/server/loopDocs.d.ts +2 -1
  240. package/dist/server/loopDocs.d.ts.map +1 -1
  241. package/dist/server/loopDocs.js +67 -51
  242. package/dist/server/loopDocs.js.map +1 -1
  243. package/dist/server/loopDocs.sql +14 -10
  244. package/dist/server/module-discovery.d.ts +2 -2
  245. package/dist/server/module-discovery.d.ts.map +1 -1
  246. package/dist/server/module-discovery.js +27 -2
  247. package/dist/server/module-discovery.js.map +1 -1
  248. package/dist/server/seam-entry-read.sql +9 -7
  249. package/dist/server/seam-log-read.sql +1 -1
  250. package/dist/server/seam-loop.sql +10 -1
  251. package/dist/server/seam-proposal-list.sql +4 -2
  252. package/dist/server/worker-capabilities.sql +19 -0
  253. package/dist/service.d.ts.map +1 -1
  254. package/dist/service.js +11 -1
  255. package/dist/service.js.map +1 -1
  256. package/migrations/001_schema.sql +486 -136
  257. package/package.json +34 -32
  258. package/dist/server/skillDocs.d.ts +0 -9
  259. package/dist/server/skillDocs.d.ts.map +0 -1
  260. package/dist/server/skillDocs.js +0 -212
  261. package/dist/server/skillDocs.js.map +0 -1
  262. package/dist/server/skillDocs.sql +0 -14
  263. package/dist/server/workspace-capabilities.sql +0 -19
@@ -19,17 +19,6 @@ CREATE TABLE IF NOT EXISTS workspaces (
19
19
 
20
20
  CREATE INDEX IF NOT EXISTS workspaces_created_at ON workspaces (created_at);
21
21
 
22
- -- {§module-workspace-state}: one opaque, provider-validated JSON snapshot per
23
- -- module owner and workspace. Core owns isolation and lifecycle only.
24
- CREATE TABLE IF NOT EXISTS workspace_module_state (
25
- workspace_id INTEGER NOT NULL,
26
- namespace_owner TEXT NOT NULL CHECK (length(namespace_owner) > 0),
27
- state TEXT NOT NULL CHECK (json_valid(state)),
28
- updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
29
- PRIMARY KEY (workspace_id, namespace_owner),
30
- FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE
31
- ) STRICT;
32
-
33
22
  -- model_routes — the immutable resolved model route ({§worker-model-selection}). One row per
34
23
  -- complete resolved tuple; append-only. Alias is provenance plus a tuning scope,
35
24
  -- not route identity, and is absent on a direct provider/model selection.
@@ -77,8 +66,11 @@ CREATE TABLE IF NOT EXISTS workers (
77
66
  -- never persist).
78
67
  settings TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(settings)),
79
68
  -- {§env-delta-log-pull}: monotonic observation progress, not a private world snapshot.
80
- -- NULL means the worker has not established its first-turn baseline yet.
69
+ -- Creation captures the workspace high-water; a fork instead copies its
70
+ -- parent's cursor and records the closed event boundary of its snapshot.
81
71
  ambient_event_cursor INTEGER CHECK (ambient_event_cursor IS NULL OR ambient_event_cursor >= 0),
72
+ fork_event_boundary INTEGER CHECK (fork_event_boundary IS NULL OR fork_event_boundary >= 0),
73
+ CHECK (fork_event_boundary IS NULL OR parent_worker_id IS NOT NULL),
82
74
  CHECK (default_conversation = 0 OR (origin = 'model' AND parent_worker_id IS NULL)),
83
75
  CHECK ((model_route_id IS NULL) = (reasoning_policy IS NULL)),
84
76
  FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE,
@@ -103,48 +95,133 @@ BEGIN
103
95
  SELECT RAISE(ABORT, 'workers.provider_identity is immutable');
104
96
  END;
105
97
 
106
- -- {§env-delta-log-pull}: one append-only occurrence journal gives every ambient
107
- -- producer a shared monotonic order. It snapshots only what the observer row
108
- -- needs; shared-world contents remain owned by entries/files, never copied here.
98
+ CREATE TRIGGER IF NOT EXISTS workers_fork_event_boundary_immutable
99
+ BEFORE UPDATE OF fork_event_boundary ON workers
100
+ WHEN NEW.fork_event_boundary IS NOT OLD.fork_event_boundary
101
+ BEGIN
102
+ SELECT RAISE(ABORT, 'workers.fork_event_boundary is immutable');
103
+ END;
104
+
105
+ -- {§module-worker-state}: one opaque, provider-validated JSON snapshot per
106
+ -- module owner and Worker. Core owns isolation, inheritance, and lifecycle;
107
+ -- the module owns its state schema and semantics.
108
+ CREATE TABLE IF NOT EXISTS worker_module_state (
109
+ worker_id INTEGER NOT NULL,
110
+ namespace_owner TEXT NOT NULL CHECK (length(namespace_owner) > 0),
111
+ state TEXT NOT NULL CHECK (json_valid(state)),
112
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
113
+ PRIMARY KEY (worker_id, namespace_owner),
114
+ FOREIGN KEY (worker_id) REFERENCES workers(id) ON DELETE CASCADE
115
+ ) STRICT;
116
+
117
+ -- A child snapshots every initialized module state at birth. Later parent
118
+ -- mutations do not propagate; the child and parent are independent authorities.
119
+ CREATE TRIGGER IF NOT EXISTS worker_module_state_inherit
120
+ AFTER INSERT ON workers
121
+ WHEN NEW.parent_worker_id IS NOT NULL
122
+ BEGIN
123
+ INSERT INTO worker_module_state (worker_id, namespace_owner, state, updated_at)
124
+ SELECT NEW.id, namespace_owner, state, updated_at
125
+ FROM worker_module_state
126
+ WHERE worker_id = NEW.parent_worker_id;
127
+ END;
128
+
129
+ -- {§env-delta-log-pull}: one append-only occurrence journal gives every
130
+ -- producer a shared monotonic order. Audience is structural: direct parent,
131
+ -- explicit workspace broadcast, or their union. The event snapshots exactly
132
+ -- what an observer row needs because source-log curation cannot erase history.
109
133
  -- source_record_id is forensic identity for the originating log/loop row, not a
110
134
  -- foreign key: model log curation must not erase an already-recorded occurrence.
111
135
  CREATE TABLE IF NOT EXISTS ambient_events (
112
- id INTEGER NOT NULL PRIMARY KEY,
113
- workspace_id INTEGER NOT NULL,
114
- producer_worker_id INTEGER NOT NULL,
115
- kind TEXT NOT NULL CHECK (kind IN ('edit', 'loop_termination')),
116
- source_record_id INTEGER NOT NULL CHECK (source_record_id >= 1),
117
- source TEXT,
118
- op TEXT NOT NULL CHECK (op IN ('EDIT', 'SEND')),
119
- scheme TEXT,
120
- hostname TEXT,
121
- pathname TEXT,
122
- rx TEXT,
123
- attrs TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(attrs)),
124
- tags TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(tags) AND json_type(tags) = 'array'),
125
- status_rx INTEGER NOT NULL CHECK (status_rx BETWEEN 100 AND 599),
126
- terminated_by TEXT CHECK (terminated_by IS NULL OR terminated_by = 'cancel'),
127
- created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
136
+ id INTEGER NOT NULL PRIMARY KEY,
137
+ workspace_id INTEGER NOT NULL,
138
+ producer_worker_id INTEGER NOT NULL,
139
+ target_parent_worker_id INTEGER,
140
+ workspace_broadcast INTEGER NOT NULL DEFAULT 0 CHECK (workspace_broadcast IN (0, 1)),
141
+ kind TEXT NOT NULL CHECK (kind IN ('activity', 'loop_termination')),
142
+ source_record_id INTEGER NOT NULL CHECK (source_record_id >= 1),
143
+ at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
144
+ source TEXT,
145
+ op TEXT NOT NULL,
146
+ delimiter TEXT NOT NULL DEFAULT '',
147
+ signal TEXT CHECK (signal IS NULL OR json_valid(signal)),
148
+ scheme TEXT,
149
+ username TEXT,
150
+ password TEXT,
151
+ hostname TEXT,
152
+ port INTEGER CHECK (port IS NULL OR (port BETWEEN 0 AND 65535)),
153
+ pathname TEXT,
154
+ query TEXT,
155
+ fragment TEXT,
156
+ line_marker TEXT CHECK (line_marker IS NULL OR json_valid(line_marker)),
157
+ tx TEXT NOT NULL,
158
+ mimetype_tx TEXT NOT NULL CHECK (length(mimetype_tx) > 0),
159
+ rx TEXT NOT NULL,
160
+ mimetype_rx TEXT NOT NULL CHECK (length(mimetype_rx) > 0),
161
+ status_rx INTEGER NOT NULL CHECK (status_rx BETWEEN 100 AND 599),
162
+ state TEXT NOT NULL CHECK (state IN ('resolved', 'failed', 'cancelled')),
163
+ outcome TEXT,
164
+ attrs TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(attrs)),
165
+ tags TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(tags) AND json_type(tags) = 'array'),
166
+ terminated_by TEXT CHECK (terminated_by IS NULL OR terminated_by = 'cancel'),
167
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
168
+ CHECK (target_parent_worker_id IS NOT NULL OR workspace_broadcast = 1),
128
169
  CHECK (
129
- (kind = 'edit' AND op = 'EDIT' AND rx IS NOT NULL AND terminated_by IS NULL)
170
+ (kind = 'activity' AND terminated_by IS NULL)
130
171
  OR
131
172
  (kind = 'loop_termination'
132
173
  AND op = 'SEND'
133
174
  AND scheme = 'worker'
134
- AND rx IS NOT NULL
135
175
  AND json_valid(rx)
136
176
  AND json_type(rx) = 'object'
137
177
  AND json_type(rx, '$.status') = 'integer'
138
178
  AND json_extract(rx, '$.status') = status_rx)
139
179
  ),
140
- FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE,
141
- FOREIGN KEY (producer_worker_id) REFERENCES workers(id) ON DELETE CASCADE
180
+ FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE,
181
+ FOREIGN KEY (producer_worker_id) REFERENCES workers(id) ON DELETE CASCADE,
182
+ FOREIGN KEY (target_parent_worker_id) REFERENCES workers(id) ON DELETE CASCADE
142
183
  ) STRICT;
143
184
 
144
185
  CREATE INDEX IF NOT EXISTS ambient_events_workspace_id_id
145
186
  ON ambient_events (workspace_id, id);
146
187
  CREATE INDEX IF NOT EXISTS ambient_events_producer_kind_id
147
188
  ON ambient_events (producer_worker_id, kind, id);
189
+ CREATE INDEX IF NOT EXISTS ambient_events_parent_id
190
+ ON ambient_events (target_parent_worker_id, id)
191
+ WHERE target_parent_worker_id IS NOT NULL;
192
+ CREATE UNIQUE INDEX IF NOT EXISTS ambient_events_source_identity
193
+ ON ambient_events (producer_worker_id, kind, source_record_id);
194
+
195
+ CREATE TRIGGER IF NOT EXISTS ambient_events_structural_audience
196
+ BEFORE INSERT ON ambient_events
197
+ WHEN NOT EXISTS (
198
+ SELECT 1
199
+ FROM workers producer
200
+ WHERE producer.id = NEW.producer_worker_id
201
+ AND producer.workspace_id = NEW.workspace_id
202
+ AND (
203
+ NEW.target_parent_worker_id IS NULL
204
+ OR NEW.target_parent_worker_id = producer.parent_worker_id
205
+ )
206
+ )
207
+ BEGIN
208
+ SELECT RAISE(ABORT, 'ambient event audience must be the producer direct parent in the same workspace');
209
+ END;
210
+
211
+ -- A worker begins after the history that predates its existence. This trigger
212
+ -- runs in the worker INSERT statement, so an occurrence is either in the
213
+ -- baseline or after it. Fork INSERTs supply their own cursor/boundary and skip
214
+ -- this ordinary-worker baseline. {§env-delta-log-pull}
215
+ CREATE TRIGGER IF NOT EXISTS workers_capture_ambient_baseline
216
+ AFTER INSERT ON workers
217
+ WHEN NEW.ambient_event_cursor IS NULL
218
+ BEGIN
219
+ UPDATE workers
220
+ SET ambient_event_cursor = COALESCE((
221
+ SELECT MAX(ae.id) FROM ambient_events ae WHERE ae.workspace_id = NEW.workspace_id
222
+ ), 0)
223
+ WHERE id = NEW.id;
224
+ END;
148
225
 
149
226
  -- loops
150
227
  -- flags: per-loop runtime flags (auto, noProposals, noWeb, noInteraction,
@@ -158,6 +235,9 @@ CREATE TABLE IF NOT EXISTS loops (
158
235
  sequence INTEGER NOT NULL CHECK (sequence >= 1),
159
236
  status INTEGER NOT NULL DEFAULT 102 CHECK (status IN (100, 102, 200, 202, 413, 429, 499, 500, 504, 508)),
160
237
  prompt TEXT NOT NULL,
238
+ -- {§prompt-causal-source}: canonical actor address for the initial prompt;
239
+ -- NULL means the owning worker itself.
240
+ prompt_source TEXT CHECK (prompt_source IS NULL OR length(prompt_source) > 0),
161
241
  flags TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(flags)),
162
242
  -- {§worker-model-selection}: immutable loop snapshots of the resolved model route and the
163
243
  -- effective spawn route (was provider_spec/child_provider_spec JSON).
@@ -237,22 +317,33 @@ BEGIN
237
317
  UPDATE loops SET terminated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = NEW.id;
238
318
  END;
239
319
 
240
- -- A terminal transition is an ambient occurrence in the same total order as a
241
- -- shared EDIT. Directly inserted fork history never crosses this transition and
320
+ -- A child terminal transition is an occurrence addressed only to its direct
321
+ -- parent. Directly inserted fork history never crosses this transition and
242
322
  -- therefore cannot fabricate a new conclusion event.
243
323
  CREATE TRIGGER IF NOT EXISTS loops_append_ambient_event
244
324
  AFTER UPDATE OF status ON loops
245
325
  WHEN NEW.status IN (200, 413, 429, 499, 500, 504, 508) AND OLD.status NOT IN (200, 413, 429, 499, 500, 504, 508)
246
326
  BEGIN
247
327
  INSERT INTO ambient_events (
248
- workspace_id, producer_worker_id, kind, source_record_id, source,
249
- op, scheme, pathname, rx, status_rx, terminated_by
328
+ workspace_id, producer_worker_id, target_parent_worker_id,
329
+ workspace_broadcast, kind, source_record_id, source,
330
+ op, delimiter, scheme, pathname,
331
+ tx, mimetype_tx, rx, mimetype_rx, status_rx, state, terminated_by
250
332
  )
251
- SELECT w.workspace_id, NEW.worker_id, 'loop_termination', NEW.id, NULL,
252
- 'SEND', 'worker', '/' || w.name, NEW.terminal_result,
253
- json_extract(NEW.terminal_result, '$.status'), NEW.terminated_by
333
+ SELECT w.workspace_id, NEW.worker_id, w.parent_worker_id,
334
+ 0, 'loop_termination', NEW.id, NULL,
335
+ 'SEND', '', 'worker', '/' || w.name,
336
+ '', 'text/plain', NEW.terminal_result, 'application/json',
337
+ json_extract(NEW.terminal_result, '$.status'), 'resolved', NEW.terminated_by
254
338
  FROM workers w
255
- WHERE w.id = NEW.worker_id;
339
+ WHERE w.id = NEW.worker_id
340
+ AND w.parent_worker_id IS NOT NULL
341
+ AND EXISTS (
342
+ SELECT 1
343
+ FROM turns t
344
+ WHERE t.loop_id = NEW.id
345
+ AND NOT (t.producer = '_plurnk' AND t.kind = 'operation')
346
+ );
256
347
  END;
257
348
 
258
349
  -- turns
@@ -268,7 +359,7 @@ CREATE TABLE IF NOT EXISTS turns (
268
359
  -- {§turn-record}: producer and purpose define the turn. Packet/provider
269
360
  -- evidence below is an optional inference specialization.
270
361
  producer TEXT NOT NULL CHECK (producer IN ('model', 'client', '_plurnk', 'plugin')),
271
- kind TEXT NOT NULL CHECK (kind IN ('inference', 'initialization', 'overflow', 'operation')),
362
+ kind TEXT NOT NULL CHECK (kind IN ('inference', 'initialization', 'overflow', 'operation', 'maintenance')),
272
363
  status INTEGER NOT NULL CHECK (status BETWEEN 100 AND 599),
273
364
  -- NULL while the producer still owns this turn. Status 102 is both the
274
365
  -- provisional running value and the exact completed continue disposition;
@@ -653,15 +744,18 @@ CREATE TABLE IF NOT EXISTS derivations (
653
744
  ) STRICT;
654
745
 
655
746
  -- entries
656
- -- The canonical addressable store. (workspace, owner, scheme, pathname) is the identity
747
+ -- The canonical addressable store. (owner, scheme, authority, pathname) is the identity
657
748
  -- tuple, and NO component may be NULL: NULLs are distinct under SQL UNIQUE, so a nullable
658
749
  -- component voids the identity index. Bare/file paths persist under the reserved `file`
659
750
  -- scheme; they still render as bare paths. {§entry-identity-no-null}
660
751
  CREATE TABLE IF NOT EXISTS entries (
661
752
  id INTEGER NOT NULL PRIMARY KEY,
662
753
  version INTEGER NOT NULL DEFAULT 0 CHECK (version >= 0),
663
- workspace_id INTEGER,
664
754
  scheme TEXT NOT NULL CHECK (length(scheme) > 0),
755
+ -- Canonical RFC authority for resource-addressed schemes. Namespace schemes
756
+ -- fold authored authority into pathname; owner-addressed schemes consume it
757
+ -- into owner_id. Empty is therefore the canonical no-resource-authority value.
758
+ authority TEXT NOT NULL DEFAULT '',
665
759
  pathname TEXT NOT NULL,
666
760
  -- {§entry-owner} — every entry is owned by a worker: the spawning worker for capability
667
761
  -- streams, the workspace's reserved 'commons' worker for shared content. A real row, never
@@ -675,9 +769,9 @@ CREATE TABLE IF NOT EXISTS entries (
675
769
  -- SPEC {§membership} — how a file member entered the curated surface. 'git' rows are
676
770
  -- reconciled against the repo's members each turn — tracked ls-files PLUS untracked-
677
771
  -- but-not-ignored files ({§membership-auto-add}) — registered + un-registered so entries
678
- -- == members; 'client'/'constraint' (model-created, add-glob) are not git's to reclaim.
772
+ -- == members; 'constraint' rows are reconciled from ordinary pick policy.
679
773
  -- NULL = not a file member (other schemes don't carry origin).
680
- membership_origin TEXT CHECK (membership_origin IS NULL OR membership_origin IN ('git', 'client', 'constraint')),
774
+ membership_origin TEXT CHECK (membership_origin IS NULL OR membership_origin IN ('git', 'constraint')),
681
775
  -- SPEC {§membership-change-gated-sync} — the per-member sync stat-detect:
682
776
  -- "<mtimeMs>:<size>" of the disk file at its last materialization, or `absent`
683
777
  -- after an observed deletion. The pre-turn
@@ -689,8 +783,6 @@ CREATE TABLE IF NOT EXISTS entries (
689
783
  -- by it ASC so dormant entries hold the stable prompt-cache prefix. Private derivation
690
784
  -- attachment does not make an entry recently touched.
691
785
  updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
692
- CHECK (workspace_id IS NOT NULL),
693
- FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE,
694
786
  FOREIGN KEY (owner_id) REFERENCES workers(id) ON DELETE CASCADE
695
787
  ) STRICT;
696
788
 
@@ -698,7 +790,8 @@ CREATE TABLE IF NOT EXISTS entries (
698
790
  -- Concurrent workers' capability streams share the loop-relative coordinate (every worker's first
699
791
  -- loop is seq 1), so identity keys on the owner and identical coordinates are distinct rows
700
792
  -- ({§stream-owner-scoped}).
701
- CREATE UNIQUE INDEX IF NOT EXISTS entries_identity ON entries (workspace_id, owner_id, scheme, pathname);
793
+ CREATE UNIQUE INDEX IF NOT EXISTS entries_identity ON entries (owner_id, scheme, authority, pathname);
794
+ CREATE UNIQUE INDEX IF NOT EXISTS entries_id_owner ON entries (id, owner_id);
702
795
 
703
796
  -- The ONE engine-imposed constraint (SPEC {§stream-constraints}, {§stream-constraints-engine-one-cap}): 100 MiB char-length cap
704
797
  -- per channel content body. All other limits are extrinsic.
@@ -867,12 +960,19 @@ CREATE TABLE IF NOT EXISTS log_entries (
867
960
  -- Engine-owned occurrence identity. Source rows are stamped NULL→id by the
868
961
  -- journal trigger; observer and fork copies carry it at insertion.
869
962
  ambient_event_id INTEGER REFERENCES ambient_events(id),
963
+ -- Fork history is copied evidence, never new activity. Rows with no source
964
+ -- occurrence still need this structural guard against republication.
965
+ inherited_history INTEGER NOT NULL DEFAULT 0 CHECK (inherited_history IN (0, 1)),
870
966
  -- Search derivation attached to this durable log result, when available.
871
967
  deep_hash TEXT REFERENCES derivations(deep_hash),
872
968
  -- Exact logical provider call represented by a BARE result, admitted
873
969
  -- model turnOps, or rejected emissionAttempt. Other operation and ambient
874
970
  -- rows carry no model-call identity.
875
971
  model_call_id INTEGER REFERENCES model_calls(id),
972
+ -- Engine-owned stream publication that produced this observation. The
973
+ -- publication cursor survives log curation; this relation is evidence,
974
+ -- never the acknowledgement itself. {§exec-stream}
975
+ subscription_publication_id INTEGER REFERENCES subscription_publications(id) ON DELETE SET NULL,
876
976
 
877
977
  -- 'error' is an ACTIONLESS row ({§operation-results} — errors are log items): a parse failure that
878
978
  -- produced no op still records a log entry (op='error', status_rx≥400, no target) so the model
@@ -938,6 +1038,9 @@ CREATE INDEX IF NOT EXISTS log_entries_at ON log_entries (a
938
1038
  CREATE UNIQUE INDEX IF NOT EXISTS log_entries_model_call_id
939
1039
  ON log_entries (model_call_id)
940
1040
  WHERE model_call_id IS NOT NULL;
1041
+ CREATE INDEX IF NOT EXISTS log_entries_subscription_publication_id
1042
+ ON log_entries (subscription_publication_id)
1043
+ WHERE subscription_publication_id IS NOT NULL;
941
1044
  CREATE UNIQUE INDEX IF NOT EXISTS log_entries_worker_ambient_event
942
1045
  ON log_entries (worker_id, ambient_event_id)
943
1046
  WHERE ambient_event_id IS NOT NULL;
@@ -1157,7 +1260,7 @@ BEGIN
1157
1260
  SELECT json_group_array(tag)
1158
1261
  FROM (SELECT tag FROM log_tags WHERE log_entry_id = NEW.id ORDER BY tag)
1159
1262
  ), '[]')
1160
- WHERE kind = 'edit'
1263
+ WHERE kind = 'activity'
1161
1264
  AND producer_worker_id = NEW.worker_id
1162
1265
  AND source_record_id = NEW.id;
1163
1266
  END;
@@ -1185,6 +1288,15 @@ BEGIN
1185
1288
  END
1186
1289
  FROM turns
1187
1290
  WHERE turns.id = NEW.turn_id;
1291
+
1292
+ UPDATE ambient_events
1293
+ SET tags = COALESCE((
1294
+ SELECT json_group_array(tag)
1295
+ FROM (SELECT tag FROM log_tags WHERE log_entry_id = NEW.id ORDER BY tag)
1296
+ ), '[]')
1297
+ WHERE kind = 'activity'
1298
+ AND producer_worker_id = NEW.worker_id
1299
+ AND source_record_id = NEW.id;
1188
1300
  END;
1189
1301
 
1190
1302
  -- Overflow admission reclassifies a would-be inference turn only after its
@@ -1204,6 +1316,24 @@ BEGIN
1204
1316
  END
1205
1317
  FROM log_entries
1206
1318
  WHERE turn_id = NEW.id;
1319
+
1320
+ UPDATE ambient_events
1321
+ SET tags = COALESCE((
1322
+ SELECT json_group_array(tag)
1323
+ FROM (
1324
+ SELECT lt.tag
1325
+ FROM log_tags lt
1326
+ WHERE lt.log_entry_id = ambient_events.source_record_id
1327
+ ORDER BY lt.tag
1328
+ )
1329
+ ), '[]')
1330
+ WHERE kind = 'activity'
1331
+ AND producer_worker_id = (
1332
+ SELECT l.worker_id FROM loops l WHERE l.id = NEW.loop_id
1333
+ )
1334
+ AND source_record_id IN (
1335
+ SELECT id FROM log_entries WHERE turn_id = NEW.id
1336
+ );
1207
1337
  END;
1208
1338
 
1209
1339
  -- Successful OPEN/FOLD rows are durable curation events even though their
@@ -1531,7 +1661,7 @@ END;
1531
1661
  -- private-payload removal cannot exempt changes to any other core column.
1532
1662
  CREATE TRIGGER IF NOT EXISTS log_entries_immutable_core
1533
1663
  BEFORE UPDATE OF
1534
- worker_id, loop_id, turn_id, sequence, at, origin, source, model_call_id,
1664
+ worker_id, loop_id, turn_id, sequence, at, origin, source, inherited_history, model_call_id,
1535
1665
  op, delimiter, signal,
1536
1666
  scheme, username, password, hostname,
1537
1667
  port, pathname, query, fragment,
@@ -1560,72 +1690,168 @@ BEGIN
1560
1690
  SELECT RAISE(ABORT, 'log_entries ambient event identity may only be assigned once');
1561
1691
  END;
1562
1692
 
1563
- -- Immediate successful shared EDITs append their occurrence in the same SQL
1564
- -- statement that persists the receipt. Rows already carrying an event id are
1565
- -- observer/fork history and can never republish themselves.
1693
+ -- One relational projection owns activity eligibility, audience, and snapshot
1694
+ -- shape for both immediately-final and proposal-settlement paths.
1695
+ CREATE VIEW IF NOT EXISTS ambient_activity_candidates AS
1696
+ SELECT *
1697
+ FROM (
1698
+ SELECT w.workspace_id,
1699
+ le.worker_id AS producer_worker_id,
1700
+ CASE
1701
+ WHEN t.producer = '_plurnk' AND t.kind = 'operation' THEN NULL
1702
+ ELSE w.parent_worker_id
1703
+ END AS target_parent_worker_id,
1704
+ CASE
1705
+ WHEN le.state = 'resolved'
1706
+ AND le.status_rx BETWEEN 200 AND 399
1707
+ AND le.status_rx != 304
1708
+ AND (
1709
+ (
1710
+ (
1711
+ le.op IN ('EDIT', 'KILL')
1712
+ OR (
1713
+ le.op = 'SEND'
1714
+ AND json_valid(le.signal)
1715
+ AND json_extract(le.signal, '$') = 410
1716
+ )
1717
+ )
1718
+ AND le.scheme = 'worker'
1719
+ AND le.hostname IS NULL
1720
+ AND le.pathname IS NOT NULL
1721
+ )
1722
+ OR (
1723
+ le.op IN ('COPY', 'MOVE')
1724
+ AND json_valid(le.rx)
1725
+ AND EXISTS (
1726
+ SELECT 1 FROM json_each(le.rx, '$.effects') effect
1727
+ WHERE json_type(effect.value) = 'object'
1728
+ AND substr(CAST(json_extract(effect.value, '$.target') AS TEXT), 1, 10) = 'worker:///'
1729
+ )
1730
+ )
1731
+ )
1732
+ THEN 1 ELSE 0
1733
+ END AS workspace_broadcast,
1734
+ le.id AS source_record_id,
1735
+ le.at,
1736
+ le.source,
1737
+ le.op,
1738
+ le.delimiter,
1739
+ le.signal,
1740
+ le.scheme,
1741
+ le.username,
1742
+ le.password,
1743
+ le.hostname,
1744
+ le.port,
1745
+ le.pathname,
1746
+ le.query,
1747
+ le.fragment,
1748
+ le.lineMarker AS line_marker,
1749
+ le.tx,
1750
+ le.mimetype_tx,
1751
+ le.rx,
1752
+ le.mimetype_rx,
1753
+ le.status_rx,
1754
+ le.state,
1755
+ le.outcome,
1756
+ json_remove(le.attrs, '$.__plurnk_curation') AS attrs,
1757
+ COALESCE((
1758
+ SELECT json_group_array(ordered.tag)
1759
+ FROM (
1760
+ SELECT tag FROM log_tags WHERE log_entry_id = le.id ORDER BY tag
1761
+ ) ordered
1762
+ ), '[]') AS tags
1763
+ FROM log_entries le
1764
+ JOIN workers w ON w.id = le.worker_id
1765
+ JOIN loops l ON l.id = le.loop_id
1766
+ JOIN turns t ON t.id = le.turn_id
1767
+ WHERE le.ambient_event_id IS NULL
1768
+ AND le.inherited_history = 0
1769
+ AND le.op IS NOT NULL
1770
+ AND le.state != 'proposed'
1771
+ AND NOT (
1772
+ le.op = 'SEND'
1773
+ AND le.scheme IS NULL
1774
+ AND l.status IN (200, 413, 429, 499, 500, 504, 508)
1775
+ )
1776
+ ) candidate
1777
+ WHERE target_parent_worker_id IS NOT NULL OR workspace_broadcast = 1;
1778
+
1779
+ -- Every final op-bearing child row is parent activity. A successful operation
1780
+ -- whose landed effects touch worker:/// additionally acquires the workspace
1781
+ -- audience. Observer rows and copied fork history cannot republish themselves.
1566
1782
  CREATE TRIGGER IF NOT EXISTS log_entries_append_ambient_event_insert
1567
1783
  AFTER INSERT ON log_entries
1568
- WHEN NEW.ambient_event_id IS NULL
1569
- AND NEW.op = 'EDIT'
1570
- AND NEW.state = 'resolved'
1571
- AND NEW.status_rx IN (200, 201)
1572
- AND (NEW.scheme IS NULL OR NEW.scheme != 'plurnk')
1573
- AND (NEW.scheme IS NULL OR NEW.scheme != 'worker' OR NEW.hostname IS NULL OR NEW.hostname = 'plurnk')
1574
- AND (
1575
- NEW.origin != '_plurnk'
1576
- OR EXISTS (SELECT 1 FROM workers w WHERE w.id = NEW.worker_id AND w.name = 'plurnk')
1577
- )
1784
+ WHEN NEW.state != 'proposed'
1578
1785
  BEGIN
1579
1786
  INSERT INTO ambient_events (
1580
- workspace_id, producer_worker_id, kind, source_record_id, source,
1581
- op, scheme, hostname, pathname, rx, attrs, tags, status_rx
1787
+ workspace_id, producer_worker_id, target_parent_worker_id,
1788
+ workspace_broadcast, kind, source_record_id, at, source,
1789
+ op, delimiter, signal,
1790
+ scheme, username, password, hostname, port, pathname, query, fragment,
1791
+ line_marker, tx, mimetype_tx, rx, mimetype_rx, status_rx,
1792
+ state, outcome, attrs, tags
1582
1793
  )
1583
- SELECT w.workspace_id, NEW.worker_id, 'edit', NEW.id, NEW.source,
1584
- 'EDIT', NEW.scheme, NEW.hostname, NEW.pathname, NEW.rx, NEW.attrs,
1585
- COALESCE((
1586
- SELECT json_group_array(ordered.tag)
1587
- FROM (
1588
- SELECT tag FROM log_tags WHERE log_entry_id = NEW.id ORDER BY tag
1589
- ) ordered
1590
- ), '[]'),
1591
- NEW.status_rx
1592
- FROM workers w
1593
- WHERE w.id = NEW.worker_id;
1594
- UPDATE log_entries SET ambient_event_id = last_insert_rowid() WHERE id = NEW.id;
1794
+ SELECT workspace_id, producer_worker_id, target_parent_worker_id,
1795
+ workspace_broadcast, 'activity', source_record_id, at, source,
1796
+ op, delimiter, signal,
1797
+ scheme, username, password, hostname, port, pathname, query, fragment,
1798
+ line_marker, tx, mimetype_tx, rx, mimetype_rx, status_rx,
1799
+ state, outcome, attrs, tags
1800
+ FROM ambient_activity_candidates
1801
+ WHERE source_record_id = NEW.id;
1802
+
1803
+ UPDATE log_entries
1804
+ SET ambient_event_id = (
1805
+ SELECT id FROM ambient_events
1806
+ WHERE producer_worker_id = NEW.worker_id
1807
+ AND kind = 'activity'
1808
+ AND source_record_id = NEW.id
1809
+ )
1810
+ WHERE id = NEW.id
1811
+ AND EXISTS (
1812
+ SELECT 1 FROM ambient_events
1813
+ WHERE producer_worker_id = NEW.worker_id
1814
+ AND kind = 'activity'
1815
+ AND source_record_id = NEW.id
1816
+ );
1595
1817
  END;
1596
1818
 
1597
- -- A proposed EDIT becomes an occurrence only when its one lifecycle transition
1598
- -- resolves successfully. Rejection, cancellation, and failed application publish nothing.
1819
+ -- A proposed operation becomes activity only when its lifecycle settles.
1599
1820
  CREATE TRIGGER IF NOT EXISTS log_entries_append_ambient_event_resolve
1600
- AFTER UPDATE OF state, status_rx ON log_entries
1601
- WHEN OLD.state = 'proposed'
1602
- AND NEW.state = 'resolved'
1603
- AND NEW.ambient_event_id IS NULL
1604
- AND NEW.op = 'EDIT'
1605
- AND NEW.status_rx IN (200, 201)
1606
- AND (NEW.scheme IS NULL OR NEW.scheme != 'plurnk')
1607
- AND (NEW.scheme IS NULL OR NEW.scheme != 'worker' OR NEW.hostname IS NULL OR NEW.hostname = 'plurnk')
1608
- AND (
1609
- NEW.origin != '_plurnk'
1610
- OR EXISTS (SELECT 1 FROM workers w WHERE w.id = NEW.worker_id AND w.name = 'plurnk')
1611
- )
1821
+ AFTER UPDATE OF state, status_rx, rx, outcome ON log_entries
1822
+ WHEN OLD.state = 'proposed' AND NEW.state != 'proposed'
1612
1823
  BEGIN
1613
1824
  INSERT INTO ambient_events (
1614
- workspace_id, producer_worker_id, kind, source_record_id, source,
1615
- op, scheme, hostname, pathname, rx, attrs, tags, status_rx
1825
+ workspace_id, producer_worker_id, target_parent_worker_id,
1826
+ workspace_broadcast, kind, source_record_id, at, source,
1827
+ op, delimiter, signal,
1828
+ scheme, username, password, hostname, port, pathname, query, fragment,
1829
+ line_marker, tx, mimetype_tx, rx, mimetype_rx, status_rx,
1830
+ state, outcome, attrs, tags
1616
1831
  )
1617
- SELECT w.workspace_id, NEW.worker_id, 'edit', NEW.id, NEW.source,
1618
- 'EDIT', NEW.scheme, NEW.hostname, NEW.pathname, NEW.rx, NEW.attrs,
1619
- COALESCE((
1620
- SELECT json_group_array(ordered.tag)
1621
- FROM (
1622
- SELECT tag FROM log_tags WHERE log_entry_id = NEW.id ORDER BY tag
1623
- ) ordered
1624
- ), '[]'),
1625
- NEW.status_rx
1626
- FROM workers w
1627
- WHERE w.id = NEW.worker_id;
1628
- UPDATE log_entries SET ambient_event_id = last_insert_rowid() WHERE id = NEW.id;
1832
+ SELECT workspace_id, producer_worker_id, target_parent_worker_id,
1833
+ workspace_broadcast, 'activity', source_record_id, at, source,
1834
+ op, delimiter, signal,
1835
+ scheme, username, password, hostname, port, pathname, query, fragment,
1836
+ line_marker, tx, mimetype_tx, rx, mimetype_rx, status_rx,
1837
+ state, outcome, attrs, tags
1838
+ FROM ambient_activity_candidates
1839
+ WHERE source_record_id = NEW.id;
1840
+
1841
+ UPDATE log_entries
1842
+ SET ambient_event_id = (
1843
+ SELECT id FROM ambient_events
1844
+ WHERE producer_worker_id = NEW.worker_id
1845
+ AND kind = 'activity'
1846
+ AND source_record_id = NEW.id
1847
+ )
1848
+ WHERE id = NEW.id
1849
+ AND EXISTS (
1850
+ SELECT 1 FROM ambient_events
1851
+ WHERE producer_worker_id = NEW.worker_id
1852
+ AND kind = 'activity'
1853
+ AND source_record_id = NEW.id
1854
+ );
1629
1855
  END;
1630
1856
 
1631
1857
  -- {§client-interactions}: the durable discoverable half of a client-owned
@@ -1652,7 +1878,8 @@ CREATE INDEX IF NOT EXISTS client_interactions_worker_id_id
1652
1878
  -- Durable subscription lifecycle per SPEC {§subscriptions}. The row records what
1653
1879
  -- the worker holds and routes cancellation to a separate process-local callable;
1654
1880
  -- it never serializes that callable. Closed rows persist for forensics; partial
1655
- -- unique index enforces one active subscription per (worker, entry).
1881
+ -- unique index enforces one active subscription per entry. The composite
1882
+ -- foreign key makes that entry part of the subscribing Worker's own space.
1656
1883
  CREATE TABLE IF NOT EXISTS subscriptions (
1657
1884
  id INTEGER NOT NULL PRIMARY KEY,
1658
1885
  version INTEGER NOT NULL DEFAULT 0 CHECK (version >= 0),
@@ -1671,14 +1898,15 @@ CREATE TABLE IF NOT EXISTS subscriptions (
1671
1898
  closed_at TEXT,
1672
1899
  close_status INTEGER CHECK (close_status IS NULL OR (close_status BETWEEN 100 AND 599)),
1673
1900
  close_result TEXT CHECK (close_result IS NULL OR json_valid(close_result)),
1674
- CHECK ((closed_at IS NULL AND close_status IS NULL)
1675
- OR (closed_at IS NOT NULL AND close_status IS NOT NULL)),
1676
- FOREIGN KEY (worker_id) REFERENCES workers(id) ON DELETE CASCADE,
1677
- FOREIGN KEY (entry_id) REFERENCES entries(id) ON DELETE CASCADE
1901
+ channel_results TEXT CHECK (channel_results IS NULL OR json_valid(channel_results)),
1902
+ CHECK ((closed_at IS NULL AND close_status IS NULL AND close_result IS NULL AND channel_results IS NULL)
1903
+ OR (closed_at IS NOT NULL AND close_status IS NOT NULL AND close_result IS NOT NULL AND channel_results IS NOT NULL)),
1904
+ FOREIGN KEY (worker_id) REFERENCES workers(id) ON DELETE CASCADE,
1905
+ FOREIGN KEY (entry_id, worker_id) REFERENCES entries(id, owner_id) ON DELETE CASCADE
1678
1906
  ) STRICT;
1679
1907
 
1680
1908
  CREATE UNIQUE INDEX IF NOT EXISTS subscriptions_active_one_per_entry
1681
- ON subscriptions (worker_id, entry_id)
1909
+ ON subscriptions (entry_id)
1682
1910
  WHERE closed_at IS NULL;
1683
1911
 
1684
1912
  CREATE INDEX IF NOT EXISTS subscriptions_scheme_active
@@ -1689,13 +1917,26 @@ CREATE INDEX IF NOT EXISTS subscriptions_opened_at ON subscriptions (opened_at);
1689
1917
 
1690
1918
  CREATE TRIGGER IF NOT EXISTS subscriptions_result_contract_insert
1691
1919
  BEFORE INSERT ON subscriptions
1920
+ WHEN NEW.closed_at IS NOT NULL
1921
+ OR NEW.close_status IS NOT NULL
1922
+ OR NEW.close_result IS NOT NULL
1923
+ OR NEW.channel_results IS NOT NULL
1924
+ BEGIN
1925
+ SELECT RAISE(ABORT, 'subscription must open before it can settle');
1926
+ END;
1927
+
1928
+ CREATE TRIGGER IF NOT EXISTS subscriptions_result_contract_update
1929
+ BEFORE UPDATE OF closed_at, close_status, close_result, channel_results ON subscriptions
1692
1930
  WHEN NOT (
1693
- (NEW.closed_at IS NULL AND NEW.close_status IS NULL AND NEW.close_result IS NULL)
1931
+ (NEW.closed_at IS NULL AND NEW.close_status IS NULL AND NEW.close_result IS NULL AND NEW.channel_results IS NULL)
1694
1932
  OR (
1695
1933
  NEW.closed_at IS NOT NULL
1696
1934
  AND NEW.close_status IS NOT NULL
1697
1935
  AND NEW.close_result IS NOT NULL
1936
+ AND NEW.channel_results IS NOT NULL
1698
1937
  AND json_valid(NEW.close_result)
1938
+ AND json_valid(NEW.channel_results)
1939
+ AND json_type(NEW.channel_results) = 'object'
1699
1940
  AND json_type(NEW.close_result, '$.status') = 'integer'
1700
1941
  AND json_extract(NEW.close_result, '$.status') = NEW.close_status
1701
1942
  AND (
@@ -1712,35 +1953,142 @@ BEGIN
1712
1953
  SELECT RAISE(ABORT, 'subscription terminal result violates the operation-result contract');
1713
1954
  END;
1714
1955
 
1715
- CREATE TRIGGER IF NOT EXISTS subscriptions_result_contract_update
1716
- BEFORE UPDATE OF closed_at, close_status, close_result ON subscriptions
1717
- WHEN NOT (
1718
- (NEW.closed_at IS NULL AND NEW.close_status IS NULL AND NEW.close_result IS NULL)
1719
- OR (
1720
- NEW.closed_at IS NOT NULL
1721
- AND NEW.close_status IS NOT NULL
1722
- AND NEW.close_result IS NOT NULL
1723
- AND json_valid(NEW.close_result)
1724
- AND json_type(NEW.close_result, '$.status') = 'integer'
1725
- AND json_extract(NEW.close_result, '$.status') = NEW.close_status
1726
- AND (
1727
- (NEW.close_status < 400 AND json_type(NEW.close_result, '$.problem') IS NULL)
1728
- OR (
1729
- NEW.close_status >= 400
1730
- AND json_type(NEW.close_result, '$.problem') = 'object'
1731
- AND json_extract(NEW.close_result, '$.problem.status') = NEW.close_status
1732
- )
1733
- )
1956
+ -- A settlement may override the universal result for exact named channels.
1957
+ -- Unknown channels and malformed results fail before the subscription or any
1958
+ -- representation changes. The JS boundary additionally rejects projection
1959
+ -- fields that are structurally unavailable in ChannelProducerResult.
1960
+ CREATE TRIGGER IF NOT EXISTS subscriptions_channel_results_contract
1961
+ BEFORE UPDATE OF closed_at, close_status, close_result, channel_results ON subscriptions
1962
+ WHEN NEW.closed_at IS NOT NULL AND (
1963
+ EXISTS (
1964
+ SELECT 1
1965
+ FROM json_each(NEW.channel_results) AS channel_result
1966
+ LEFT JOIN entry_channels ec
1967
+ ON ec.entry_id = NEW.entry_id AND ec.name = channel_result.key
1968
+ WHERE ec.name IS NULL
1969
+ OR json_type(channel_result.value) != 'object'
1970
+ OR json_type(channel_result.value, '$.status') != 'integer'
1971
+ OR json_extract(channel_result.value, '$.status') NOT BETWEEN 200 AND 599
1972
+ OR json_extract(channel_result.value, '$.status') = 202
1973
+ OR (
1974
+ json_extract(channel_result.value, '$.status') < 400
1975
+ AND json_type(channel_result.value, '$.problem') IS NOT NULL
1976
+ )
1977
+ OR (
1978
+ json_extract(channel_result.value, '$.status') >= 400
1979
+ AND (
1980
+ json_type(channel_result.value, '$.problem') != 'object'
1981
+ OR json_extract(channel_result.value, '$.problem.status')
1982
+ != json_extract(channel_result.value, '$.status')
1983
+ )
1984
+ )
1734
1985
  )
1735
1986
  )
1736
1987
  BEGIN
1737
- SELECT RAISE(ABORT, 'subscription terminal result violates the operation-result contract');
1988
+ SELECT RAISE(ABORT, 'subscription channel result violates the channel producer contract');
1989
+ END;
1990
+
1991
+ -- Subscription settlement is the single atomic transition that closes the
1992
+ -- lifecycle and installs current terminal producer evidence on every channel.
1993
+ -- Overrides are exact; all other channels inherit the universal result.
1994
+ CREATE TRIGGER IF NOT EXISTS subscriptions_settle_channels
1995
+ AFTER UPDATE OF closed_at, close_status, close_result, channel_results ON subscriptions
1996
+ WHEN OLD.closed_at IS NULL AND NEW.closed_at IS NOT NULL
1997
+ BEGIN
1998
+ UPDATE entry_channels
1999
+ SET producer_result = COALESCE(
2000
+ (
2001
+ SELECT json(channel_result.value)
2002
+ FROM json_each(NEW.channel_results) AS channel_result
2003
+ WHERE channel_result.key = entry_channels.name
2004
+ ),
2005
+ NEW.close_result
2006
+ ),
2007
+ state = CASE WHEN json_extract(
2008
+ COALESCE(
2009
+ (
2010
+ SELECT json(channel_result.value)
2011
+ FROM json_each(NEW.channel_results) AS channel_result
2012
+ WHERE channel_result.key = entry_channels.name
2013
+ ),
2014
+ NEW.close_result
2015
+ ),
2016
+ '$.status'
2017
+ ) >= 400 THEN 'errored' ELSE 'closed' END
2018
+ WHERE entry_id = NEW.entry_id;
2019
+ END;
2020
+
2021
+ -- One durable publication cursor per selected subscription channel. Log rows
2022
+ -- are curated model context and therefore cannot own this lifecycle fact.
2023
+ -- The row survives KILLing any generated observation and disappears only with
2024
+ -- its subscription. {§exec-stream}
2025
+ CREATE TABLE IF NOT EXISTS subscription_publications (
2026
+ id INTEGER NOT NULL PRIMARY KEY,
2027
+ version INTEGER NOT NULL DEFAULT 0 CHECK (version >= 0),
2028
+ subscription_id INTEGER NOT NULL,
2029
+ channel TEXT NOT NULL CHECK (length(channel) > 0),
2030
+ published_end INTEGER NOT NULL DEFAULT 0 CHECK (published_end >= 0),
2031
+ terminal_published INTEGER NOT NULL DEFAULT 0 CHECK (terminal_published IN (0, 1)),
2032
+ UNIQUE (subscription_id, channel),
2033
+ FOREIGN KEY (subscription_id) REFERENCES subscriptions(id) ON DELETE CASCADE
2034
+ ) STRICT;
2035
+
2036
+ CREATE INDEX IF NOT EXISTS subscription_publications_pending
2037
+ ON subscription_publications (subscription_id, terminal_published);
2038
+
2039
+ CREATE TRIGGER IF NOT EXISTS subscriptions_seed_publications
2040
+ AFTER INSERT ON subscriptions
2041
+ BEGIN
2042
+ INSERT INTO subscription_publications (subscription_id, channel)
2043
+ SELECT NEW.id, ec.name
2044
+ FROM entry_channels ec
2045
+ WHERE ec.entry_id = NEW.entry_id
2046
+ AND (NEW.published_channel IS NULL OR ec.name = NEW.published_channel);
2047
+
2048
+ SELECT CASE WHEN NOT EXISTS (
2049
+ SELECT 1 FROM subscription_publications WHERE subscription_id = NEW.id
2050
+ ) THEN RAISE(ABORT, 'subscription has no publishable channel') END;
2051
+ END;
2052
+
2053
+ -- The generated READ and its cursor transition are one SQLite statement. A
2054
+ -- later log KILL removes evidence from active context without rewinding the
2055
+ -- subscription/channel publication state. {§exec-stream}
2056
+ CREATE TRIGGER IF NOT EXISTS log_entries_advance_subscription_publication
2057
+ AFTER INSERT ON log_entries
2058
+ WHEN NEW.subscription_publication_id IS NOT NULL
2059
+ BEGIN
2060
+ SELECT CASE WHEN
2061
+ NEW.origin != '_plurnk'
2062
+ OR NEW.op != 'READ'
2063
+ OR json_type(NEW.attrs, '$.streamEnd') != 'integer'
2064
+ OR json_extract(NEW.attrs, '$.streamEnd') < 0
2065
+ OR json_type(NEW.attrs, '$.terminal') NOT IN ('true', 'false')
2066
+ THEN RAISE(ABORT, 'subscription publication requires one canonical stream observation') END;
2067
+
2068
+ UPDATE subscription_publications
2069
+ SET published_end = json_extract(NEW.attrs, '$.streamEnd'),
2070
+ terminal_published = json_extract(NEW.attrs, '$.terminal'),
2071
+ version = version + 1
2072
+ WHERE id = NEW.subscription_publication_id
2073
+ AND terminal_published = 0
2074
+ AND (
2075
+ (json_extract(NEW.attrs, '$.terminal') = 0
2076
+ AND json_extract(NEW.attrs, '$.streamEnd') > published_end)
2077
+ OR
2078
+ (json_extract(NEW.attrs, '$.terminal') = 1
2079
+ AND json_extract(NEW.attrs, '$.streamEnd') >= published_end)
2080
+ );
2081
+
2082
+ SELECT CASE WHEN changes() != 1
2083
+ THEN RAISE(ABORT, 'subscription publication transition is stale or already terminal')
2084
+ END;
1738
2085
  END;
1739
2086
 
1740
2087
  -- (worker_watermarks removed — {§env-delta} is now pull-from-log, no per-worker snapshot.)
1741
2088
 
1742
2089
  -- workspace_constraints
1743
- -- SPEC {§membership} constraint overlay — the client's supersede over git membership.
2090
+ -- SPEC {§membership} constraint overlay — explicit policy and the inspectable
2091
+ -- exact pick produced by accepted file creation share one representation.
1744
2092
  -- Per (workspace, effect, glob/target): `pick` (members git misses, resolved by a
1745
2093
  -- targeted client-dictated scan), `hide` (drop git-tracked matches), `view` (member
1746
2094
  -- for read; File.edit rejects the write). git-absent, `pick` rows are the sole substrate
@@ -1749,6 +2097,8 @@ CREATE TABLE IF NOT EXISTS workspace_constraints (
1749
2097
  workspace_id INTEGER NOT NULL,
1750
2098
  effect TEXT NOT NULL CHECK (effect IN ('pick', 'hide', 'view')),
1751
2099
  glob TEXT NOT NULL,
2100
+ source TEXT NOT NULL CHECK (source IN ('explicit', 'create')),
2101
+ CHECK (source = 'explicit' OR effect = 'pick'),
1752
2102
  PRIMARY KEY (workspace_id, effect, glob),
1753
2103
  FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE
1754
2104
  ) STRICT, WITHOUT ROWID;