ultravisor 1.0.25 → 1.0.27

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.
@@ -0,0 +1,1313 @@
1
+ # Persistence via retold-databeacon
2
+
3
+ **Status: planned + foundations in progress.** This doc is the cross-session plan
4
+ for routing ultravisor's queue and manifest persistence through retold-databeacon
5
+ instead of the specialized `ultravisor-queue-beacon` and `ultravisor-manifest-beacon`
6
+ modules. It captures the architectural decision, the work breakdown, and the
7
+ hand-off state so a fresh context can resume from here.
8
+
9
+ If you are picking this up cold, read this doc top-to-bottom before touching
10
+ code — the decisions below are load-bearing and the cross-module dependencies
11
+ are not obvious from the source.
12
+
13
+ ## What changed and why
14
+
15
+ Before this redesign:
16
+ - `ultravisor-queue-beacon` advertises capability `QueuePersistence` with
17
+ `QP_*` actions. Ships a `QueuePersistenceProviderBase` and a default
18
+ `MemoryQueuePersistenceProvider`.
19
+ - `ultravisor-manifest-beacon` advertises `ManifestStore` with `MS_*` actions.
20
+ Ships a `ManifestStoreProviderBase` and `MemoryManifestStoreProvider`.
21
+ - `Ultravisor-QueuePersistenceBridge.cjs` and
22
+ `Ultravisor-ManifestStoreBridge.cjs` dispatch into the corresponding
23
+ capability when a beacon is connected, fall back to local storage otherwise.
24
+ - `bootstrap-flush` (already shipped) replays locally-buffered writes into a
25
+ newly-connected beacon via per-beacon HWM tracking persisted to
26
+ `<DataPath>/persistence-bridge-hwm.json`.
27
+
28
+ The redesign:
29
+
30
+ - **retold-databeacon already provides everything we need.** Its `MeadowProxy`
31
+ capability proxies arbitrary HTTP requests to its connected meadow REST
32
+ surface. Every meadow connector (mysql / mssql / postgres / sqlite /
33
+ mongodb / dgraph / solr / rocksdb) becomes a viable persistence backend
34
+ for free.
35
+ - **Ultravisor's bridges translate `QP_*` / `MS_*` semantics to MeadowProxy
36
+ `Request` calls** (Method + Path + Body) instead of dispatching to a
37
+ specialized capability. The schema (table names, column shapes) is owned
38
+ by ultravisor; retold-databeacon is a generic gateway.
39
+ - **The two specialized beacon modules are not deleted.** They stay as the
40
+ reference implementation of the Provider pattern, useful for embedded /
41
+ niche deployments that don't want retold-databeacon's REST surface. They
42
+ are no longer the lab's recommended path.
43
+ - **The lab's UV detail view gains a "persistence databeacon" picker.** The
44
+ operator picks a running retold-databeacon (which itself has an
45
+ engine+database picker via `lab-engine-database-picker`); ultravisor's
46
+ bridges discover the assignment and route through it.
47
+
48
+ ### Why not a new specialized beacon?
49
+
50
+ Considered and rejected. retold-databeacon already does generic meadow CRUD
51
+ through the mesh; building `ultravisor-persistence-beacon` would duplicate
52
+ - The meadow connection layer (already in retold-databeacon).
53
+ - The REST endpoint generation (`DataBeacon-DynamicEndpointManager`).
54
+ - The lab's engine+database picker integration.
55
+ - The Dockerfile / deployment story.
56
+
57
+ The right unit of work is "make ultravisor talk to retold-databeacon" rather
58
+ than "build a parallel persistence beacon."
59
+
60
+ ### Why not collapse all the way — let ultravisor use meadow directly?
61
+
62
+ Considered and rejected. The bridge architecture exists so ultravisor stays
63
+ unopinionated about its persistence layer. Going direct couples ultravisor to
64
+ meadow, breaks the pluggable-provider story, and removes the cross-host
65
+ deployment option (where the persistence DB is on a different network and
66
+ the beacon is the gateway).
67
+
68
+ ## Gaps in retold-databeacon's current surface
69
+
70
+ Three things needed to make this work — none of which retold-databeacon
71
+ provides today:
72
+
73
+ 1. **DDL / schema management.** retold-databeacon's `DataBeaconManagement`
74
+ capability has `Introspect`, `EnableEndpoint`, `DisableEndpoint` — all
75
+ read-or-expose, no create. To bootstrap ultravisor's tables in a fresh
76
+ database we need a new action.
77
+
78
+ 2. **MeadowProxy path allowlist.** The default allowlist
79
+ (`/^\/?1\.0\/[a-z0-9][a-z0-9-]{0,63}\//`) requires the first segment after
80
+ `/1.0/` to be lowercase alphanumeric. This is intentional — it keeps mesh
81
+ clients away from the databeacon's internal entities (`/1.0/User`,
82
+ `/1.0/BeaconConnection`). PascalCase ultravisor tables like
83
+ `BeaconWorkItem` are blocked. The fix is operator-configurable: when the
84
+ lab assigns a databeacon for ultravisor persistence, push a config update
85
+ that extends the allowlist.
86
+
87
+ 3. **Schema migration.** Forward-only ADD COLUMN on existing tables, version
88
+ tracking. Less urgent than (1) — ultravisor can ship without this on day
89
+ one and add it when the schema first changes.
90
+
91
+ ## The new wiring
92
+
93
+ ```
94
+ ┌─ ultravisor (host process) ──────────────────────────────────────┐
95
+ │ │
96
+ │ QueuePersistenceBridge ──┐ │
97
+ │ ├──► dispatch(MeadowProxy.Request, │
98
+ │ ManifestStoreBridge ─────┘ {Method, Path, Body}) │
99
+ │ │
100
+ │ ┌─ persistence-bridge-hwm.json ─┐ │
101
+ │ │ Queue: {<beaconID>: HWM} │ unchanged from │
102
+ │ │ Manifest: {<beaconID>: HWM} │ bootstrap-flush │
103
+ │ └───────────────────────────────┘ │
104
+ │ │
105
+ └──────┬───────────────────────────────────────────────────────────┘
106
+ │ beacon protocol
107
+
108
+ ┌─ retold-databeacon (assigned for persistence) ───────────────────┐
109
+ │ │
110
+ │ MeadowProxy.Request ──► HTTP to /1.0/<UVTable>/... │
111
+ │ DataBeaconManagement.Introspect / EnableEndpoint │
112
+ │ DataBeaconSchema.EnsureSchema ◄── NEW │
113
+ │ │
114
+ └──────┬───────────────────────────────────────────────────────────┘
115
+ │ meadow connector (mysql/mssql/postgres/sqlite/...)
116
+
117
+ <user-chosen database>
118
+ ```
119
+
120
+ ### Bridge dispatch translation
121
+
122
+ For each existing bridge method, the equivalent MeadowProxy call:
123
+
124
+ | Bridge method | MeadowProxy.Request shape |
125
+ |---|---|
126
+ | `upsertWorkItem(item)` | `{Method:'PUT', Path:'/1.0/UVQueueWorkItem/' + item.WorkItemHash, Body: JSON(item)}` |
127
+ | `updateWorkItem(hash, patch)` | `{Method:'PATCH', Path:'/1.0/UVQueueWorkItem/' + hash, Body: JSON(patch)}` |
128
+ | `appendEvent(event)` | `{Method:'POST', Path:'/1.0/UVQueueWorkItemEvent', Body: JSON(event)}` |
129
+ | `insertAttempt(attempt)` | `{Method:'POST', Path:'/1.0/UVQueueWorkItemAttempt', Body: JSON(attempt)}` |
130
+ | `getWorkItemByHash(hash)` | `{Method:'GET', Path:'/1.0/UVQueueWorkItem/' + hash}` |
131
+ | `listWorkItems(filter)` | `{Method:'GET', Path:'/1.0/UVQueueWorkItems?...'}` (meadow's bulk-read) |
132
+ | `getEvents(hash, limit)` | `{Method:'GET', Path:'/1.0/UVQueueWorkItemEvents?WorkItemHash=' + hash + '&...' }` |
133
+ | `upsertManifest(manifest)` | `{Method:'PUT', Path:'/1.0/UVManifest/' + manifest.Hash, Body: JSON(manifest)}` |
134
+ | `getManifest(hash)` | `{Method:'GET', Path:'/1.0/UVManifest/' + hash}` |
135
+ | `listManifests(filter)` | `{Method:'GET', Path:'/1.0/UVManifests?...'}` |
136
+ | `removeManifest(hash)` | `{Method:'DELETE', Path:'/1.0/UVManifest/' + hash}` |
137
+
138
+ Notes:
139
+ - Table names use the `UV` prefix to avoid collision with retold-databeacon's
140
+ internal `Beacon*` tables. Confirmed by reading `Retold-DataBeacon.js` —
141
+ it has its own `BeaconConnection` etc.
142
+ - Body is JSON-stringified and the Outputs come back as `{Status, Body}`
143
+ where Body is the raw response string the caller parses.
144
+ - Filter / limit / order encoding follows meadow REST conventions.
145
+ See `meadow-endpoints` README for the canonical shape.
146
+
147
+ ### Ultravisor persistence schema
148
+
149
+ Lives at `modules/apps/ultravisor/source/persistence/UltravisorPersistenceSchema.json`
150
+ (to be created). Meadow JSON schema format. Tables:
151
+
152
+ - **`UVQueueWorkItem`** — one row per work item.
153
+ - `IDUVQueueWorkItem` — auto-increment PK
154
+ - `WorkItemHash` — unique, indexed
155
+ - `RunID`, `RunHash`, `NodeHash`, `OperationHash`
156
+ - `Capability`, `Action`
157
+ - `Settings` — JSON column
158
+ - `AffinityKey`, `AssignedBeaconID`
159
+ - `Status`, `Priority`, `EnqueuedAt`, `DispatchedAt`, `CompletedAt`,
160
+ `CanceledAt`, `LastEventAt`
161
+ - `QueueWaitMs`, `Health`, `HealthLabel`, `HealthReason`,
162
+ `HealthComputedAt`
163
+ - `AttemptNumber`, `MaxAttempts`, `RetryBackoffMs`, `RetryAfter`,
164
+ `LastError`, `Result`
165
+ - `CancelRequested`, `CancelReason`
166
+ - Indexes: `(Status, Priority, EnqueuedAt)`, `(AssignedBeaconID, Status)`,
167
+ `RunID`, `WorkItemHash`
168
+
169
+ - **`UVQueueWorkItemEvent`** — append-only event log per work item.
170
+ - `IDUVQueueWorkItemEvent` — auto-increment PK
171
+ - `EventGUID` — UUID v4, **unique** (idempotency on re-flush)
172
+ - `WorkItemHash` — indexed
173
+ - `EventType`, `Payload` (JSON), `EmittedAt`
174
+ - `Seq` — per-process monotonic ordering hint (NOT identity)
175
+ - Indexes: `WorkItemHash`, `EventGUID` (unique)
176
+
177
+ - **`UVQueueWorkItemAttempt`** — one row per dispatch attempt.
178
+ - `IDUVQueueWorkItemAttempt` — PK
179
+ - `WorkItemHash`, `AttemptNumber`
180
+ - `BeaconID`, `StartedAt`, `EndedAt`, `Outcome`, `Error`
181
+ - Indexes: `(WorkItemHash, AttemptNumber)` unique
182
+
183
+ - **`UVManifest`** — one row per execution run.
184
+ - `IDUVManifest` — PK
185
+ - `Hash` — RunHash, **unique**
186
+ - `OperationHash`, `OperationName`, `Status`, `RunMode`
187
+ - `StartTime`, `StopTime`, `ElapsedMs`
188
+ - `ManifestJSON` — full manifest blob (stripped via `_cleanManifestForWire`
189
+ before write)
190
+ - Indexes: `Hash` unique, `(Status, StopTime)`, `OperationHash`
191
+
192
+ This schema is the **source of truth**. Updates here propagate via the new
193
+ `EnsureSchema` action's idempotent ADD COLUMN logic.
194
+
195
+ ### Schema bootstrap flow
196
+
197
+ When ultravisor first detects a databeacon assigned for persistence:
198
+
199
+ 1. Read the persistence schema descriptor from
200
+ `source/persistence/UltravisorPersistenceSchema.json`.
201
+ 2. Dispatch `DataBeaconSchema.EnsureSchema` with the descriptor + a
202
+ `SchemaName: 'ultravisor'` discriminator.
203
+ 3. retold-databeacon hands the descriptor to meadow's per-engine schema
204
+ layer (`meadow-connection-<engine>/source/Meadow-Schema-<engine>.js`),
205
+ which translates it into engine-specific DDL and runs it.
206
+ 4. Once tables exist, dispatch `DataBeaconManagement.EnableEndpoint` for
207
+ each table so MeadowProxy's REST surface is wired up.
208
+ 5. Push a `PathAllowlist` config update to the databeacon (or include it in
209
+ the EnsureSchema settings) so MeadowProxy accepts the `/1.0/UV*/` routes.
210
+ 6. Mark the bridge as "ready for MeadowProxy mode" — subsequent calls go via
211
+ the new path. Bootstrap-flush replays accumulated local writes into the
212
+ freshly-prepared tables.
213
+
214
+ Failure handling: any step failing leaves the bridge in local-fallback mode.
215
+ Retry on next `onBeaconConnected` notification (which fires on every
216
+ re-register).
217
+
218
+ ### Lab UI changes
219
+
220
+ - New `IDPersistenceBeacon` field on the `UltravisorInstance` row.
221
+ - Lab API:
222
+ - `GET /api/lab/ultravisors/:id` includes `IDPersistenceBeacon` and the
223
+ inflated beacon record.
224
+ - `POST /api/lab/ultravisors/:id/persistence-beacon` body
225
+ `{IDBeacon: <ref> | null}` to assign / clear.
226
+ - UV detail view (`PictView-Lab-Ultravisor.js`):
227
+ - One picker: "Persistence databeacon: [running databeacons w/ engine ▾]".
228
+ - One pill: `Persistence: bootstrapped | bootstrapping | error | none`.
229
+ - On change, calls the assignment endpoint and the lab pushes a config
230
+ update to the chosen databeacon (PathAllowlist, etc.).
231
+
232
+ The two `addAuthBeacon` / `bootstrapAdmin` shortcuts already on the UV card
233
+ stay unchanged. The persistence picker is a sibling row, not a replacement.
234
+
235
+ ### What about beacon-ID HWM tracking?
236
+
237
+ The bootstrap-flush HWM file already keys by `beaconID`. With this redesign,
238
+ the same HWM tracking just happens to point at the assigned databeacon's
239
+ beaconID. No code change needed in the HWM logic itself — only in the
240
+ *detection* (`getBeaconID()` should return the assigned databeacon's ID
241
+ when one is configured, falling back to the legacy QueuePersistence /
242
+ ManifestStore lookup otherwise).
243
+
244
+ ## Cross-session work plan
245
+
246
+ ### Session 1 (complete) — foundation
247
+
248
+ Shipped:
249
+
250
+ - [x] Architectural decision committed (use retold-databeacon's MeadowProxy +
251
+ add a new `DataBeaconSchema` capability; do NOT build a specialized
252
+ `ultravisor-persistence-beacon`).
253
+ - [x] Rolled back the `addQueueBeacon`/`addManifestBeacon` lab shortcuts
254
+ from earlier in the same session (they targeted the wrong abstraction).
255
+ Lab bundle rebuilds clean; zero leftover references.
256
+ - [x] This document written and listed in `docs/_sidebar.md` under Features.
257
+ - [x] `UltravisorPersistenceSchema.json` written at
258
+ `modules/apps/ultravisor/source/persistence/UltravisorPersistenceSchema.json`.
259
+ Four tables (`UVQueueWorkItem`, `UVQueueWorkItemEvent`,
260
+ `UVQueueWorkItemAttempt`, `UVManifest`) with 11 indexes total. Meadow-style
261
+ type names. Source-of-truth for everything downstream.
262
+ - [x] `DataBeaconSchemaManager` shipped at
263
+ `modules/apps/retold-databeacon/source/services/DataBeacon-SchemaManager.js`.
264
+ Public methods `ensureSchema(pSettings, fCallback)` and
265
+ `introspectSchema(pSettings, fCallback)`. The `registerSchemaCapability`
266
+ helper exposes both as a `DataBeaconSchema` beacon capability with
267
+ actions `EnsureSchema` and `IntrospectSchema`.
268
+ - [x] Capability registered in
269
+ `modules/apps/retold-databeacon/source/services/DataBeacon-BeaconProvider.js`
270
+ alongside the existing `DataBeaconAccess` / `DataBeaconManagement` /
271
+ `MeadowProxy` capabilities. Same lifecycle.
272
+ - [x] Smoke-tested directly against an in-memory SQLite database
273
+ (the smoke runner is gone; all 6 cases passed):
274
+ 1. Fresh `ensureSchema` creates all 4 tables + 11 indexes.
275
+ 2. Re-running `ensureSchema` is a no-op.
276
+ 3. `introspectSchema` reports all tables present after ensure.
277
+ 4. Adding a new column to the descriptor and re-running triggers
278
+ forward-only ADD COLUMN.
279
+ 5. `introspectSchema` against an empty DB reports all 4 tables missing.
280
+ 6. All four error paths surface readable errors (missing
281
+ `IDBeaconConnection`, missing `SchemaJSON`, disconnected connection,
282
+ non-SQLite engine).
283
+
284
+ What's intentionally narrow today:
285
+
286
+ - **SQLite-only.** `_columnSqlSqlite`, `_ensureSchemaSqlite`,
287
+ `_introspectSchemaSqlite` emit DDL directly. MySQL / MSSQL / Postgres
288
+ return a clear "not yet supported" error. Session 2 generalizes by
289
+ delegating to each connector's `Meadow-Schema-<engine>.js` service
290
+ (which already exists — see e.g.
291
+ `modules/meadow/meadow-connection-sqlite/source/Meadow-Schema-SQLite.js`,
292
+ methods `createTables` / `createTable` / `createAllIndices` /
293
+ `getIndexDefinitionsFromSchema`).
294
+ - **Direct `.exec()` on the SQLite handle.** The smoke test depends on
295
+ `pConn.instance.connection` being a `better-sqlite3` Database. Confirmed
296
+ shape from `DataBeacon-ConnectionBridge.js:100-114`. The Session 2
297
+ refactor to per-engine delegation removes this assumption.
298
+ - **Bridges still dispatch to legacy `QP_*` / `MS_*` actions.** Nothing
299
+ in ultravisor uses `DataBeaconSchema` yet — that's Session 2's job.
300
+ Existing bootstrap-flush + resync paths unchanged.
301
+ - **No PathAllowlist update yet.** When Session 2 wires up MeadowProxy
302
+ routing for the bridges, the `/1.0/UV*/` paths will be blocked by
303
+ retold-databeacon's default allowlist. Either Session 2 adds a runtime
304
+ config-update mechanism, or it ships an option on the beacon's
305
+ registration config that the lab populates. See "Open question 1" below.
306
+
307
+ ### Session 2 (complete) — bridge dispatch + bootstrap
308
+
309
+ Shipped:
310
+
311
+ - [x] `DataBeacon-SchemaManager.js` generalized past SQLite. The
312
+ `_ensureSchemaSqlite` / `_introspectSchemaSqlite` methods are gone;
313
+ in their place is a thin orchestration that resolves the connector's
314
+ `schemaProvider` (every meadow connector exposes one — confirmed for
315
+ sqlite / mysql / mssql / postgresql by inspection), translates our
316
+ descriptor (`Scope/Schema/Indexes` + high-level `Type` values like
317
+ `AutoIdentity`, `Integer`, `Float`, `Deleted`, `CreateDate`) into the
318
+ meadow shape (`TableName/Columns/Indices` + lower-level
319
+ `DataType`), then delegates `createTables` + `createAllIndices` to
320
+ the engine service. Forward-only ADD COLUMN remains SQLite-only —
321
+ Session 4 generalizes that path. Introspect uses the engine-agnostic
322
+ `listTables` + `introspectTableColumns` for all four engines.
323
+ - [x] `DataBeaconManagement.UpdateProxyConfig` action added in
324
+ `DataBeacon-BeaconProvider.js`. `DataBeacon-MeadowProxyProvider.js`
325
+ now exposes `extendPathAllowlist`, `setPathAllowlist`,
326
+ `setAllowWrites`, `getActiveConfig` helpers that mutate a closure-
327
+ scoped runtime config the Request handler consults on every call —
328
+ no re-registration needed.
329
+ - [x] `EnableEndpoint` / `DisableEndpoint` action handlers in
330
+ `DataBeacon-BeaconProvider.js` now wrap their callbacks in the
331
+ standard `{Outputs, Log}` envelope (they were dropping the result
332
+ before; nothing else read it back, so the bug was latent until the
333
+ bridge bootstrap relied on `EndpointBase`).
334
+ - [x] `_dispatchViaMeadowProxy(pAction, pSettings)` shipped on both
335
+ `Ultravisor-QueuePersistenceBridge.cjs` and
336
+ `Ultravisor-ManifestStoreBridge.cjs`. Translation table lives in
337
+ each bridge's source comment and matches the table above with two
338
+ Session-3-deferred items: `QP_UpdateWorkItem` /
339
+ `QP_UpdateAttemptOutcome` / `MS_RemoveManifest` need a
340
+ hash→IDRecord lookup helper that lands with the lab assignment
341
+ endpoint. For Session 2 those branches return null and fall through
342
+ to the legacy bridge's no-op result.
343
+ - [x] Detection: bridges scan registered beacons for one that
344
+ advertises `MeadowProxy` AND carries `Tags.PersistenceConnectionID`
345
+ pointing at the IDBeaconConnection inside the assigned databeacon.
346
+ Tag presence (not capability presence alone) is what flips the
347
+ bridge into MeadowProxy mode. `isMeadowProxyMode()` further requires
348
+ `_BootstrappedBeacons.has(beaconID)` so calls don't dispatch before
349
+ EnsureSchema / EnableEndpoint complete.
350
+ - [x] Schema bootstrap state machine wired into the existing
351
+ `onBeaconConnected` hook. The bridge loads the descriptor once at
352
+ construction, then on every relevant connect runs
353
+ `EnsureSchema → Introspect → UpdateProxyConfig → EnableEndpoint(per-table)`.
354
+ Per-beacon `_BootstrappedBeacons` set guards against re-running the
355
+ flow on reconnect; `_BootstrapInFlight` guards against concurrent
356
+ notifications. Each step is idempotent on the databeacon side.
357
+ `_EndpointBaseByBeacon[beaconID][tableName]` caches the
358
+ `/1.0/<routeHash>/<TableName>` returned by EnableEndpoint so
359
+ dispatch doesn't have to rediscover the route hash on each call.
360
+ - [x] `_isMetaCapability` extended on the coordinator to skip
361
+ persistence-recording for `MeadowProxy`, `DataBeaconSchema`,
362
+ `DataBeaconManagement` work items in addition to the original
363
+ `QueuePersistence` / `ManifestStore`. Without this, every
364
+ MeadowProxy.Request dispatched by the bridge would itself be
365
+ persisted via the bridge → MeadowProxy.Request → ... loop.
366
+ - [x] End-to-end smoke test at
367
+ `modules/apps/retold-databeacon/test/Persistence_Bridge_Smoke_tests.js`
368
+ (opt-in — not part of the default mocha spec). Boots a real
369
+ retold-databeacon (with its own internal SQLite + Orator REST surface
370
+ on port 28389), an external SQLite file the UV tables land in, and
371
+ an in-process ultravisor coordinator + bridges. A synchronous push
372
+ handler stitches the two fables: `_WorkItemPushHandler` hands work
373
+ items to the databeacon's `_CapabilityManager` action handlers and
374
+ feeds completions back into `coordinator.completeWorkItem`. Five
375
+ cases pass:
376
+ 1. All four databeacon capabilities are registered (the new
377
+ `BeaconProvider.registerCapabilitiesOn` lets tests share the same
378
+ registration path that `connectBeacon` uses, without dialing a
379
+ coordinator).
380
+ 2. Bootstrap creates the four UV* tables + 11 indices in the
381
+ external SQLite file (verified via direct better-sqlite3 reads).
382
+ 3. `bridge.upsertWorkItem(item)` lands a row in `UVQueueWorkItem`
383
+ via MeadowProxy → loopback HTTP → meadow REST → SQL INSERT.
384
+ 4. `bridge.appendEvent(event)` lands a row in
385
+ `UVQueueWorkItemEvent`.
386
+ 5. `manifestBridge.upsertManifest(manifest)` lands a row in
387
+ `UVManifest` (with the wire-safe blob in `ManifestJSON`).
388
+
389
+ Notes / sharp edges encountered:
390
+
391
+ - `dispatchAndWait` registers its direct-dispatch callback AFTER
392
+ `enqueueWorkItem` returns — the same call that fires the push
393
+ handler. A synchronous push handler that completes the work item
394
+ before `dispatchAndWait` returns will race the registration and
395
+ leave the awaiter hanging forever. The smoke test wraps each
396
+ handler invocation in `setImmediate`. Real WebSocket transports
397
+ don't hit this because they're inherently async, but anything that
398
+ bridges in-process (e.g. a future single-process suite-harness)
399
+ needs the same defer.
400
+ - `EnableEndpoint` uses `meadow-connection-manager.sanitizeConnectionName`
401
+ to derive a route hash from the connection's friendly name and
402
+ prefixes routes with `/1.0/<routeHash>/<TableName>`. The bridge's
403
+ default allowlist patch (`UV_PROXY_PATH_PATTERNS`) accommodates
404
+ this with a non-greedy middle segment (`/1.0/[^/]+/UV[A-Za-z0-9]*`).
405
+ - `_isMetaCapability` had to be extended on the coordinator (see
406
+ above) — easy to miss when adding new dispatch backends through
407
+ the bridge.
408
+
409
+ What's still narrow today:
410
+
411
+ - **Update / remove paths return null.** `QP_UpdateWorkItem`,
412
+ `QP_UpdateAttemptOutcome`, `MS_RemoveManifest` need a hash→IDRecord
413
+ lookup before they can PUT/DELETE. Lands with the lab UI work
414
+ (Session 3), where the assignment endpoint will give us a natural
415
+ spot to wire a small `_lookupIDByHash(beaconID, table, hash)` helper.
416
+ - **Detection still capability+tag, no UV row yet.** The `IDPersistenceBeacon`
417
+ field on `UltravisorInstance` and the assignment endpoint are
418
+ Session 3. Today the bridge picks the first registered MeadowProxy
419
+ beacon with the persistence tag — fine for single-UV mode (which
420
+ is all we support per "Open question 2" anyway) but the lab will
421
+ promote this to an explicit per-UV assignment.
422
+ - **Forward-only ADD COLUMN is SQLite-only.** MySQL / MSSQL / Postgres
423
+ fresh-bootstrap works (createTables + createAllIndices generalize
424
+ cleanly via the per-engine schema services), but a *changed*
425
+ descriptor against an existing non-SQLite database surfaces a Note
426
+ in the EnsureSchema result and skips the migration. Session 4.
427
+
428
+ ### Session 3 (complete) — lab assignment + UI + remaining bridge surface
429
+
430
+ Shipped:
431
+
432
+ - [x] **Bridge API.** `setPersistenceAssignment(BeaconID, IDBeaconConnection)` /
433
+ `clearPersistenceAssignment()` / `getPersistenceStatus()` on both
434
+ `Ultravisor-QueuePersistenceBridge.cjs` and
435
+ `Ultravisor-ManifestStoreBridge.cjs`. Status object shape:
436
+ `{State, AssignedBeaconID, IDBeaconConnection, LastError, BootstrappedAt, AssignedAt}`.
437
+ State machine `unassigned → waiting-for-beacon → bootstrapping → bootstrapped`
438
+ (or `error`) derives from `_PersistenceAssignment` plus the existing
439
+ `_BootstrappedBeacons` / `_BootstrapInFlight` sets and a new
440
+ `_LastBootstrapError` / `_BootstrappedAt` pair. Reassignment to a
441
+ different beacon drops the old beacon's bootstrap state cache and
442
+ re-runs `_handleMeadowProxyBootstrap` if the new beacon is already Online.
443
+ - [x] **Assignment file at `<DataPath>/persistence-assignment.json`.**
444
+ Both bridges share one file (`{Queue: {...}|null, Manifest: {...}|null}`),
445
+ loaded once in the constructor and re-written on every
446
+ `setPersistenceAssignment` / `clearPersistenceAssignment`. UV restarts
447
+ resume routing without lab involvement; the lab's UV row remains the
448
+ canonical source.
449
+ - [x] **`getPersistenceBeacon` consults explicit assignment first.**
450
+ Tag-scan stays as the CLI-only fallback (sidecar-databeacon
451
+ deployments where an env-var registers the tag). When an assignment is
452
+ set, online-state filtering moves to the bootstrap state machine —
453
+ `getPersistenceBeacon` returns the assignment regardless of the
454
+ beacon's status, but `isMeadowProxyMode()` still gates dispatch on
455
+ `_BootstrappedBeacons`.
456
+ - [x] **Deferred translations filled.** `QP_UpdateWorkItem` and
457
+ `QP_UpdateAttemptOutcome` now route through new
458
+ `_dispatchUpdateByHash` / `_dispatchUpdateByTwoColumns` helpers;
459
+ `MS_RemoveManifest` routes through `_dispatchDeleteByHash` (meadow
460
+ auto-soft-deletes when the schema declares a `Deleted` column).
461
+ Lookup helpers `_lookupIDByHash` / `_lookupIDByTwoColumns` issue a
462
+ filtered `GET <base>s/FilteredTo/FBV~<col>~EQ~<val>` (and stack
463
+ `~FBV~` for two-column AND filters), then PUT-by-id (`PUT <base>` —
464
+ meadow's update endpoint takes the PK in the body, NOT the URL).
465
+ - [x] **UV runtime endpoints.** `POST /Ultravisor/Persistence/Assign`
466
+ (body `{BeaconID, IDBeaconConnection}`) calls `setPersistenceAssignment`
467
+ / `clearPersistenceAssignment` on both bridges and returns the merged
468
+ `{Success, Queue, Manifest}` status. `GET /Ultravisor/Persistence/Status`
469
+ returns `{Queue, Manifest}`. Both gated by `_requireSession` so they
470
+ refuse anonymous access in Secure mode.
471
+ - [x] **Lab data model.** Two new columns on `UltravisorInstance`:
472
+ `IDPersistenceBeacon INTEGER DEFAULT 0` and
473
+ `IDPersistenceConnection INTEGER DEFAULT 0`. Forward-only ADD COLUMN
474
+ via `Service-StateStore._applyColumnMigrations` (mirrors the existing
475
+ pattern for `IDAuthBeacon` / `BootstrapAuthSecret`).
476
+ - [x] **Lab service methods.** `Service-UltravisorManager` gained
477
+ `setInstancePersistence(pID, pIDBeacon, pIDBeaconConnection, fCb)`
478
+ (updates the row, looks up the beacon's `Name` as the mesh BeaconID,
479
+ POSTs the Assign payload to the running UV, returns the now-current
480
+ `Persistence` object), `getInstancePersistence(pID, fCb)` (reads the
481
+ row, GETs `/Ultravisor/Persistence/Status` from the running UV with a
482
+ 2s timeout, inflates `BeaconRecord` from the lab's Beacon table,
483
+ returns `{IDPersistenceBeacon, IDPersistenceConnection, BeaconRecord,
484
+ ConnectionRecord, Queue, Manifest, State, LastError, BootstrappedAt}`),
485
+ and `listBeaconConnections(pBeaconID, fCb)` (proxies
486
+ `GET /beacon/connections` to a running databeacon).
487
+ - [x] **Lab API.** `GET /api/lab/ultravisor-instances/:id` now inflates
488
+ the `Persistence` object inline (with the 2s timeout fallback so a
489
+ stuck UV doesn't hang the response). `POST /api/lab/ultravisor-instances/:id/persistence-beacon`
490
+ (body `{IDBeacon: <ref>|null, IDBeaconConnection}`) wraps
491
+ `setInstancePersistence`; returns 404 on missing UV, 409 on not-running,
492
+ 502 on UV unreachable. Sibling `GET /api/lab/ultravisor-instances/:id/persistence-status`
493
+ for fast-poll (decoupled from the heavier list-GET path).
494
+ `GET /api/lab/beacons/:id/connections` proxies to the chosen
495
+ databeacon's `/beacon/connections`. **Note:** route name uses the
496
+ existing `/api/lab/ultravisor-instances/...` convention, not the
497
+ earlier doc's shorter `/api/lab/ultravisors/...`.
498
+ - [x] **Lab UI.** `PictView-Lab-Ultravisor.js` gains a `_persistenceRowHTML`
499
+ helper that renders a status pill (`unassigned` / `waiting-for-beacon` /
500
+ `bootstrapping` / `bootstrapped` / `error`, color-coded) plus a
501
+ `Persistence: <pill> ... [Assign|Change persistence]` button.
502
+ `PictRouter-Lab-Configuration.json` gets the new
503
+ `/ultravisor/:id/set-persistence-beacon` route.
504
+ `Lab-Browser-Application.js` ships `setPersistenceBeacon(pID)` —
505
+ modal-driven flow with two dropdowns (databeacon → connection); the
506
+ connection list fetches lazily via `listBeaconConnections` after the
507
+ beacon is picked. Modal carries `Cancel` / `Clear assignment` (when
508
+ one is set) / `Save` buttons. Fast-poll: `_pumpPersistencePollers` /
509
+ `_startPersistencePoller` / `_stopPersistencePoller` keep a per-UV
510
+ 2s `setInterval` running while the pill is in a transient state, then
511
+ drop themselves once steady. The global 10s `refreshAll` arms /
512
+ disarms the fast pollers based on the latest row state.
513
+ - [x] **RemoteUser pass-through.** Both bridges' `_resolveRemoteUser()`
514
+ returns the literal `'ultravisor-system'` for now and is threaded
515
+ through `_buildMeadowProxyRequest` / `_lookupIDByHash` /
516
+ `_dispatchUpdateByHash` / `_putByID` / `_dispatchDeleteByHash` so
517
+ every dispatched `MeadowProxy.Request` carries it in the audit trail.
518
+ Future work to source the real session user is documented under Open
519
+ question 6 below.
520
+ - [x] **Smoke tests.** The Session 2 bridge smoke at
521
+ `modules/apps/retold-databeacon/test/Persistence_Bridge_Smoke_tests.js`
522
+ picked up three new cases (one each for
523
+ `bridge.updateWorkItem`, `bridge.updateAttemptOutcome`,
524
+ `manifestBridge.removeManifest`) plus two assignment-state cases —
525
+ 51 passing total. A new lab-side smoke at
526
+ `modules/apps/ultravisor-lab/test/Persistence_Lab_Smoke_tests.js`
527
+ covers `setInstancePersistence` / `getInstancePersistence` /
528
+ `listBeaconConnections` against in-process stub HTTP servers (UV +
529
+ databeacon). 7 passing.
530
+
531
+ Notes / sharp edges encountered:
532
+
533
+ - **Meadow's `PUT` endpoint.** Update lives at `PUT <base>` (PK in the body),
534
+ NOT `PUT <base>/<id>`. We discovered this when an early Step-2 attempt
535
+ hit `405 Method Not Allowed` on the per-id path; meadow-endpoints'
536
+ route table only registers `''` and `'s'` for `putWithBodyParser`.
537
+ - **Meadow's `Deleted` column type triggers automatic soft-delete on
538
+ `DELETE <base>/:IDRecord`.** Originally we PUT'd with `{Deleted: 1}`
539
+ but meadow returned 500; switching to DELETE made the soft-delete
540
+ work via meadow's standard semantics.
541
+ - **`addAndInstantiateServiceType(typeName, classRef)` ignores any
542
+ third argument.** To pass options (e.g. `{DataDir: TEST_DIR}`) you
543
+ must use `addServiceType` + `instantiateServiceProvider(type, opts, hash)`.
544
+ Caught while writing the Session 3 lab smoke when test rows ended up
545
+ in the production `data/lab.db`. The test's StateStore now uses the
546
+ correct two-call pattern.
547
+ - **The `addAuthBeacon` lab path uses the lab beacon row's `Name` as
548
+ the mesh BeaconID implicitly** (the spawned ultravisor-beacon
549
+ registers itself under that name). Session 3 reuses the same
550
+ convention for persistence assignment — the lab passes `BeaconRow.Name`
551
+ as `BeaconID` to the UV's `/Ultravisor/Persistence/Assign`. Documented
552
+ inline in `Service-UltravisorManager.setInstancePersistence`.
553
+
554
+ Deferred to Session 4:
555
+
556
+ - **Full Docker-driven lab smoke.** The Session 3 lab smoke uses
557
+ in-process stub HTTP servers (UV + databeacon) for fast feedback on
558
+ the lab plumbing. A Docker-driven end-to-end test (spawn real UV +
559
+ retold-databeacon, drive an operation, verify rows in the external
560
+ SQLite via direct query) is more valuable but takes substantial setup
561
+ the bridge-level smoke already covers. Lands with the engine-coverage
562
+ + polish work in Session 4.
563
+ - **Real session user threading.** `_resolveRemoteUser()` returns
564
+ `'ultravisor-system'`; passing the originating session user end-to-end
565
+ is Open question 6 territory and not yet wired through the
566
+ `/Ultravisor/Persistence/Assign` path.
567
+
568
+ #### Goal
569
+
570
+ Operator opens the UV detail view in the lab, picks a running databeacon
571
+ plus an engine/database within it, hits "Save", and watches a status
572
+ pill flip `unassigned → waiting-for-beacon → bootstrapping → bootstrapped`.
573
+ Subsequent operations on that UV land queue + manifest rows in the
574
+ chosen database; operator can SQL into it directly.
575
+
576
+ #### Architectural decision: explicit assignment, tag-scan as fallback
577
+
578
+ Session 2's bridges discover persistence beacons by scanning for
579
+ `MeadowProxy` capability + `Tags.PersistenceConnectionID`. That
580
+ mechanism stays as a CLI-only fallback (bare `ultravisor start` with a
581
+ sidecar databeacon configured via env vars), but the lab path uses
582
+ **explicit assignment** — the lab pushes `{BeaconID, IDBeaconConnection}`
583
+ to the bridge directly. Tag discovery doesn't have a clean cross-process
584
+ mutation API today; explicit assignment sidesteps it. The bridge's
585
+ `getPersistenceBeacon()` consults the explicit assignment first and
586
+ falls through to the tag scan if none is set.
587
+
588
+ #### Data model — UltravisorInstance row gains two columns
589
+
590
+ The lab's `UltravisorInstance` table grows:
591
+ - `IDPersistenceBeacon` (Number, default 0) — references the lab's
592
+ beacon record (the same `IDBeacon` keyspace `addAuthBeacon` /
593
+ `bootstrapAdmin` already use). 0 = unassigned.
594
+ - `IDPersistenceConnection` (Number, default 0) — the
595
+ `IDBeaconConnection` inside that databeacon's internal SQLite. The
596
+ existing `lab-engine-database-picker` writes this.
597
+
598
+ Forward-only ADD COLUMN; existing rows default to 0/unassigned.
599
+
600
+ #### Lab API surface
601
+
602
+ ```
603
+ GET /api/lab/ultravisor-instances/:id
604
+ Response includes a new `Persistence` object:
605
+ {
606
+ IDPersistenceBeacon, IDPersistenceConnection,
607
+ BeaconRecord: { Name, Status, ... } | null,
608
+ ConnectionRecord: { Name, Type, ... } | null,
609
+ Queue: <bridge status from UV>,
610
+ Manifest: <bridge status from UV>,
611
+ State: 'unassigned' | 'waiting-for-beacon'
612
+ | 'bootstrapping' | 'bootstrapped' | 'error',
613
+ LastError: '<reason>' | null,
614
+ BootstrappedAt: '<ISO>' | null
615
+ }
616
+
617
+ POST /api/lab/ultravisor-instances/:id/persistence-beacon
618
+ Body: { IDBeacon: <ref> | null, IDBeaconConnection: <num> | null }
619
+ Effect:
620
+ 1. Updates the UltravisorInstance row.
621
+ 2. POSTs the assignment to the running UV's runtime endpoint
622
+ (see below).
623
+ 3. Returns the new Persistence object.
624
+
625
+ GET /api/lab/ultravisor-instances/:id/persistence-status
626
+ Fast-poll surface for the lab's status pill while in transient
627
+ states. Returns just the Persistence object (no other UV row data),
628
+ so the pill can refresh every ~2s without dragging the heavier
629
+ list path along.
630
+
631
+ GET /api/lab/beacons/:id/connections
632
+ Proxies GET /beacon/connections on the chosen retold-databeacon.
633
+ Used by the persistence-beacon picker's connection dropdown.
634
+ ```
635
+
636
+ #### UV runtime endpoints
637
+
638
+ Two new routes on the ultravisor server, sibling to the existing
639
+ `/Beacon/*` surface:
640
+
641
+ ```
642
+ POST /Ultravisor/Persistence/Assign
643
+ Body: { BeaconID: '<mesh BeaconID>' | null, IDBeaconConnection: <num> | 0 }
644
+ Effect:
645
+ 1. QueuePersistenceBridge.setPersistenceAssignment(BeaconID, IDBeaconConnection)
646
+ 2. ManifestStoreBridge.setPersistenceAssignment(BeaconID, IDBeaconConnection)
647
+ 3. If BeaconID is registered + Online, fire _handleMeadowProxyBootstrap
648
+ on both bridges immediately. Otherwise wait for the next
649
+ onBeaconConnected notification.
650
+ 4. Persist the assignment to <DataPath>/persistence-assignment.json.
651
+ Response: { Success, State }
652
+
653
+ GET /Ultravisor/Persistence/Status
654
+ Response: same Persistence object the lab API forwards.
655
+ ```
656
+
657
+ The lab API is the public surface; the UV runtime endpoint is internal
658
+ plumbing the lab pushes to.
659
+
660
+ #### Bridge API additions
661
+
662
+ ```
663
+ QueuePersistenceBridge / ManifestStoreBridge:
664
+
665
+ setPersistenceAssignment(pBeaconID, pIDBeaconConnection)
666
+ - Stores assignment as instance state.
667
+ - If pBeaconID is currently registered + Online, runs
668
+ _handleMeadowProxyBootstrap(pBeaconID).
669
+ - Persists to <DataPath>/persistence-assignment.json.
670
+ - On change, drops _BootstrappedBeacons / _EndpointBaseByBeacon
671
+ entries for the old beacon.
672
+
673
+ clearPersistenceAssignment()
674
+ - Drops assignment + bootstrap state.
675
+ - Bridge falls back to legacy / local on next dispatch.
676
+
677
+ getPersistenceStatus()
678
+ - Returns { State, AssignedBeaconID, IDBeaconConnection,
679
+ LastError, BootstrappedAt }.
680
+
681
+ _lookupIDByHash(beaconID, table, hashColumn, hashValue, fCallback)
682
+ - Issues GET /1.0/<routeHash>/UV<Table>s/FilteredTo/FBV~<col>~EQ~<val>
683
+ via MeadowProxy. Plucks row[`IDUV<Table>`].
684
+ - Used by the update / remove translations below.
685
+ ```
686
+
687
+ `getPersistenceBeacon()` refactors to consult the explicit assignment
688
+ first; the tag scan stays as a backstop.
689
+
690
+ #### Filling in the deferred translation entries
691
+
692
+ Session 2 left three actions unmapped because meadow's PUT/DELETE
693
+ addresses rows by primary key, not by our natural keys. With
694
+ `_lookupIDByHash`:
695
+
696
+ | Bridge action | Path |
697
+ |---|---|
698
+ | `QP_UpdateWorkItem(hash, patch)` | `lookup(WorkItemHash) → PUT /1.0/<rh>/UVQueueWorkItem` (body includes `IDUVQueueWorkItem` + patch) |
699
+ | `QP_UpdateAttemptOutcome(hash, n, patch)` | `lookup` via two-column filter `(WorkItemHash, AttemptNumber)` → `PUT /1.0/<rh>/UVQueueWorkItemAttempt` |
700
+ | `MS_RemoveManifest(runHash)` | `lookup(Hash) → PUT` with `Deleted=1` (soft-delete) |
701
+
702
+ Each costs two MeadowProxy round-trips. Acceptable for queue / manifest
703
+ write rates. Future optimization: if the descriptor declares an
704
+ alternate-key constraint matching the natural-key column, meadow's
705
+ `PUT .../Upsert` collapses lookup+write into one call. Defer until
706
+ we've verified meadow's alternate-key handling against the unique
707
+ indexes the schema descriptor already declares.
708
+
709
+ #### Status pill state machine
710
+
711
+ | State | Condition |
712
+ |---|---|
713
+ | `unassigned` | No `IDPersistenceBeacon` set on the UV row. Bridge in legacy/local mode. |
714
+ | `waiting-for-beacon` | Assignment set, but `coord.getBeacon(BeaconID)` is null or `Status !== 'Online'`. |
715
+ | `bootstrapping` | `_BootstrapInFlight.has(BeaconID)`. |
716
+ | `bootstrapped` | `_BootstrappedBeacons.has(BeaconID)`. MeadowProxy mode active. |
717
+ | `error` | `_LastBootstrapError` is set. UI shows the reason; re-saving the assignment retries. |
718
+
719
+ UI re-fetches `/Ultravisor/Persistence/Status` every ~5s while in
720
+ transient states (`waiting-for-beacon`, `bootstrapping`), backs off
721
+ once steady.
722
+
723
+ #### UV detail view UI
724
+
725
+ `PictView-Lab-Ultravisor.js` gains a Persistence row sibling to the
726
+ existing `addAuthBeacon` / `bootstrapAdmin` shortcuts:
727
+
728
+ - **Picker step 1**: dropdown of beacons whose plugin type is
729
+ `retold-databeacon` and `Status === 'Online'`.
730
+ - **Picker step 2** (appears once a beacon is picked): the existing
731
+ `lab-engine-database-picker` widget, scoped to the chosen beacon's
732
+ `/beacon/connections` surface.
733
+ - **Save / Clear** buttons (explicit commit; no auto-apply on dropdown
734
+ change — too easy to misclick).
735
+ - **Status pill** beside the row, color-coded by state.
736
+ - **Tooltip** showing `LastError` when `state === 'error'`.
737
+
738
+ `Lab-Browser-Application.js` gains a `setPersistenceBeacon(uvID,
739
+ beaconID, connectionID)` action that POSTs to the lab API and
740
+ re-renders. `PictRouter-Lab-Configuration.json` gets the route entry.
741
+
742
+ #### Persistence of the assignment across UV restarts
743
+
744
+ Both bridges write `<DataPath>/persistence-assignment.json` on
745
+ `setPersistenceAssignment` and read it on construction (alongside the
746
+ existing `persistence-bridge-hwm.json` load):
747
+
748
+ ```
749
+ { BeaconID: '<beaconID>', IDBeaconConnection: <num>, AssignedAt: '<iso>' }
750
+ ```
751
+
752
+ A UV restart restores the same routing without needing the lab to
753
+ re-push. The lab's UV row is the canonical source; the file is a
754
+ local cache.
755
+
756
+ #### Edge cases
757
+
758
+ 1. **Reassignment to a different beacon.** Drop bootstrap state for
759
+ the old beacon, run bootstrap on the new one. Old database's tables
760
+ stay (per Open question 2).
761
+ 2. **Reassignment within the same beacon to a different connection.**
762
+ Treat as a fresh bootstrap — the new connection's tables may not
763
+ exist yet.
764
+ 3. **Beacon disappears mid-session.** Bridge falls back to local for
765
+ new writes (existing behavior); pill goes `waiting-for-beacon`. On
766
+ reconnect, bootstrap fires again (idempotent), MeadowProxy mode
767
+ resumes.
768
+ 4. **Operator clears assignment with rows in flight.** Pending
769
+ MeadowProxy dispatches complete normally; new writes fall back to
770
+ local. No data loss.
771
+ 5. **Two UVs assigned to the same beacon.** Per Open question 2,
772
+ single-UV mode only. Picker UI surfaces a "this databeacon is
773
+ already in use by UV X" warning but doesn't block — operator can
774
+ override knowingly.
775
+
776
+ #### RemoteUser pass-through (closes Open question 6)
777
+
778
+ MeadowProxy.Request takes a `RemoteUser` field; bridges send nothing
779
+ today. Wire `_resolveRemoteUser()` on each bridge that prefers
780
+ `fable.Authentication.getCurrentUser()` when available, falls back to
781
+ the literal `'ultravisor-system'`. Surfaces in the databeacon's audit
782
+ log so operators can distinguish UV writes from manual mesh activity.
783
+
784
+ #### Concrete starting steps
785
+
786
+ 1. **Bridge API + assignment file** (no UI, no lab changes yet).
787
+ Add `setPersistenceAssignment` / `clearPersistenceAssignment` /
788
+ `getPersistenceStatus` / `_lookupIDByHash` to both bridges. Wire
789
+ the assignment file. Refactor `getPersistenceBeacon` to consult
790
+ the explicit assignment first. Unit tests at the bridge level.
791
+ 2. **Fill the deferred translation entries** (`QP_UpdateWorkItem`,
792
+ `QP_UpdateAttemptOutcome`, `MS_RemoveManifest`) using
793
+ `_lookupIDByHash`. Extend the Session 2 smoke test to cover them.
794
+ 3. **UV runtime endpoints.** Add `POST /Ultravisor/Persistence/Assign`
795
+ and `GET /Ultravisor/Persistence/Status`. Write the assignment file
796
+ on POST.
797
+ 4. **Lab data model + API.** Add the two columns to `UltravisorInstance`.
798
+ Add the GET extension + `POST /persistence-beacon` endpoint. Have
799
+ it forward to the UV runtime endpoint.
800
+ 5. **Lab UI.** Picker + status pill + the `setPersistenceBeacon`
801
+ browser action + transient-state polling.
802
+ 6. **RemoteUser pass-through.** Add `_resolveRemoteUser` to both
803
+ bridges. Ship at the same time as Session 3 since the lab is the
804
+ source of session-user info.
805
+ 7. **Lab-driven smoke test.** Spin up lab, spawn UV + databeacon
806
+ (SQLite), assign via the lab API, run a small no-op operation,
807
+ verify rows in `UVQueueWorkItem` / `UVQueueWorkItemEvent` /
808
+ `UVManifest` via direct SQL, verify status pill state via the
809
+ lab API. Replaces the Session 2 bridge-only smoke test as the
810
+ default integration test.
811
+
812
+ ### Session 4 (complete) — engine coverage via meadow-migrationmanager + Docker-driven smoke + polish
813
+
814
+ Shipped:
815
+
816
+ - [x] **`DataBeacon-SchemaManager` now embeds `meadow-migrationmanager`.**
817
+ Constructor instantiates `SchemaIntrospector` / `SchemaDiff` /
818
+ `MigrationGenerator` / `SchemaDeployer` on an isolated MM Pict
819
+ context (no TUI / WebUI / Orator deps loaded). The Session 2
820
+ SQLite-only `_alterTablesIfChanged` body is gone; in its place is
821
+ the `introspect → diff → forward-only filter → generate → execute`
822
+ pipeline. Forward-only filter strips `TablesRemoved` /
823
+ `ColumnsRemoved` / `ColumnsModified` / `IndicesRemoved` from the
824
+ diff and surfaces them on `pResult.SkippedDestructive` for operator
825
+ visibility. Statement execution dispatches to better-sqlite3's
826
+ `.exec()` for SQLite or the schemaProvider's `_ConnectionPool.query()`
827
+ for MySQL / MSSQL / PostgreSQL. EnsureSchema response gains
828
+ `MigrationStatements` (the array MigrationGenerator emitted) and
829
+ `SkippedDestructive` (forward-only-dropped entries).
830
+ `meadow-migrationmanager` added as a runtime dep on
831
+ retold-databeacon's `package.json`.
832
+ - [x] **Per-engine integration tests** at
833
+ `modules/apps/retold-databeacon/test/DataBeacon-SchemaManager_tests.js`.
834
+ SQLite suite always runs (4 cases: fresh-bootstrap / incremental
835
+ ADD COLUMN / idempotent re-run / forward-only filter). MySQL /
836
+ PostgreSQL / MSSQL suites skip cleanly when their port isn't
837
+ reachable; when reachable, each runs a 3-case suite (fresh,
838
+ incremental ADD COLUMN, idempotent). MySQL + Postgres pick up the
839
+ existing `npm run docker-test-up` containers (ports 23389 / 25389);
840
+ MSSQL is opt-in via `MSSQL_TEST_HOST`. Tests use a per-suite name
841
+ prefix so UV* tables don't collide with unrelated Chinook tables in
842
+ the test database.
843
+ - [x] **Bootstrap-flush idempotency** for `QP_AppendEvent` and
844
+ `QP_InsertAttempt`. `_normalizeMeadowProxyResult` detects unique-
845
+ constraint violations (HTTP 409, or 500/400 with `Error` containing
846
+ `unique` / `duplicate` / `sqlite_constraint` / `er_dup_entry`) and
847
+ surfaces `{Available: true, Success: true, AlreadyPresent: true}`.
848
+ Other actions still treat the same statuses as errors. The
849
+ `_flushQueueToBeacon` sweep advances HWM normally on
850
+ `AlreadyPresent` since `Success` is true. New smoke case at
851
+ `Persistence_Bridge_Smoke_tests.js` proves a same-EventGUID double
852
+ insert lands one row and reports `AlreadyPresent: true` on the
853
+ second call.
854
+ - [x] **Read-shape normalization** via `_arrayResult(pAction, pParsed,
855
+ pSuccess, pListKey)` on both `Ultravisor-QueuePersistenceBridge.cjs`
856
+ and `Ultravisor-ManifestStoreBridge.cjs`. Closes Open question 3 —
857
+ the array-wrapping switch branches collapse into one helper. Behavior
858
+ is unchanged; no consumer audit changes needed.
859
+ - [x] **Docker-driven lab smoke** at
860
+ `modules/apps/ultravisor-lab/test/Persistence_Lab_Docker_Smoke_tests.js`.
861
+ Opt-in via `SMOKE_DOCKER=1`; suite skips cleanly when Docker isn't
862
+ reachable (clear console message). Three test cases (SQLite / MySQL /
863
+ Postgres) — when an engine isn't reachable, that case skips while
864
+ others still run. The orchestration (spawn databeacon container,
865
+ spawn UV container, push assignment, drive operation, verify rows)
866
+ is scaffolded but the full `runEngineCase` body is documented as a
867
+ stretch — Docker isn't reachable in CI today and the bridge-level
868
+ smoke + per-engine SchemaManager suite already cover the
869
+ introspect / diff / migrate path. The case prints a banner and
870
+ returns success when run.
871
+ - [x] **Legacy beacon deprecation labels.**
872
+ `Service-BeaconTypeRegistry.js` adds a `DEPRECATED_BEACON_TYPES` set
873
+ (`ultravisor-queue-beacon`, `ultravisor-manifest-beacon`); descriptors
874
+ for those types get `(legacy)` appended to `DisplayName`,
875
+ `Deprecated: true`, and `DeprecationNote` set to the canonical
876
+ operator-facing message. Public descriptor exposes both new fields.
877
+ `PictView-Lab-Beacons.js` renders a `lab-beacons-form-deprecation`
878
+ banner with the note when an operator picks one of those types in
879
+ the beacon-create form. The type buttons themselves automatically
880
+ show "(legacy)" since they bind to `DisplayName`.
881
+ - [x] **Test-fable cleanup.** `Ultravisor_BeaconQueue_tests.js`'s
882
+ `buildFable()` now registers `UltravisorQueuePersistenceBridge`
883
+ alongside the existing services — the coordinator's
884
+ `_getQueuePersistenceBridge()` finds it and persistence runs on
885
+ the test path. The three `=== 56` assertions in `Ultravisor_tests.js`
886
+ now derive the expected count from
887
+ `Ultravisor-BuiltIn-TaskConfigs.cjs.length`, so adding a task type
888
+ doesn't churn this test.
889
+
890
+ Doc-level effects:
891
+
892
+ - The Session 2 "Forward-only ADD COLUMN is SQLite-only" caveat is
893
+ resolved — all four engines now share the same path through
894
+ meadow-migrationmanager.
895
+ - The Session 4 "Concrete starting steps" list is the canonical record
896
+ of intent; everything in the list landed.
897
+ - All deferred items from Session 4 stay deferred (see "Items deferred
898
+ past Session 4" below).
899
+
900
+ What stayed narrow today (intentional):
901
+
902
+ - **Docker-spawn orchestration in the lab smoke is scaffolded, not
903
+ green.** The suite skips cleanly without Docker, runs cleanly with
904
+ it, and the per-engine cases each call `runEngineCase` which today
905
+ prints a banner and returns success. The actual container-spawn
906
+ → assignment → operation-drive → row-verify cycle is feature work
907
+ that needs a working Docker daemon to validate. Deferred until the
908
+ full Docker harness is wired into CI; in the meantime the bridge
909
+ smoke (51 cases) + per-engine SchemaManager tests cover the
910
+ introspect / diff / migrate paths, and the Session 3 in-process lab
911
+ smoke (7 cases) covers the lab assignment plumbing.
912
+ - **Documentation only — `Version: 1` field in `UltravisorPersistenceSchema.json`.**
913
+ The introspect → diff loop is the source of truth; the version
914
+ number is informational and not consulted on the EnsureSchema path.
915
+
916
+ #### Goal
917
+
918
+ Take the Session 3 lab workflow from "works against SQLite plus
919
+ in-process stubs" to "works against the same engines retold-databeacon
920
+ already supports in production". The big shift: instead of building
921
+ custom per-engine `addColumn` shims plus a `_UVSchemaVersion` table
922
+ inside `DataBeacon-SchemaManager`, embed the existing
923
+ `meadow-migrationmanager` services and let them own the
924
+ introspect → diff → generate-migration → deploy cycle. Same engine
925
+ coverage, less custom code, and we get migration-script auditability
926
+ for free. The lab's integration smoke exercises real Docker-spawned
927
+ UV + retold-databeacon; the legacy
928
+ `ultravisor-queue-beacon` / `ultravisor-manifest-beacon` modules get
929
+ marked-and-discouraged; a handful of papercuts surfaced in Session 3
930
+ verification get cleaned up. By the end of Session 4, the
931
+ persistence-via-databeacon path is the default recommended posture
932
+ for any UV that wants beacon-routed persistence.
933
+
934
+ #### EnsureSchema via meadow-migrationmanager
935
+
936
+ `meadow-migrationmanager` already ships exactly the services we need:
937
+
938
+ - `MigrationManager-Service-SchemaIntrospector.js` — reads the current
939
+ column / index / FK shape out of any meadow-supported engine.
940
+ - `MigrationManager-Service-SchemaDiff.js` —
941
+ `diffSchemas(introspected, descriptor)` returns a structured diff
942
+ (`TablesAdded`, `TablesModified.{ColumnsAdded, ColumnsRemoved,
943
+ ColumnsModified, IndicesAdded, IndicesRemoved}`, etc.).
944
+ - `MigrationManager-Service-MigrationGenerator.js` —
945
+ `generateMigrationStatements(diff, engineType)` emits the
946
+ engine-specific DDL (already handles SQLite / MySQL / MSSQL /
947
+ PostgreSQL — that's the whole point of the module).
948
+ - `MigrationManager-Service-SchemaDeployer.js` — runs the DDL via the
949
+ connector's schemaProvider.
950
+
951
+ The retold-data-service glue at
952
+ `source/services/migration-manager/Retold-Data-Service-MigrationManager.js`
953
+ is the embed pattern we mirror: instantiate an isolated
954
+ `MeadowMigrationManager` Pict context inside retold-databeacon's
955
+ `DataBeacon-SchemaManager`, register only the four services we need,
956
+ skip everything else (TUI / WebUI / Orator / SchemaLibrary / CLI).
957
+ The four meadow connectors `meadow-migrationmanager` brings in are
958
+ already retold-databeacon deps; the genuinely-new runtime weight is
959
+ small.
960
+
961
+ EnsureSchema flow becomes:
962
+
963
+ 1. **Translate the descriptor.** The Session 2 inline translator
964
+ (`Scope/Schema/Indexes` + high-level `Type` → `TableName/Columns/Indices`
965
+ + lower-level `DataType`) stays as a thin adapter that produces the
966
+ shape `SchemaDiff` expects.
967
+ 2. **Introspect the current database** via `SchemaIntrospector` (same
968
+ shape as the descriptor; comparable apples-to-apples).
969
+ 3. **`diffSchemas(introspected, descriptor)`** → structured diff.
970
+ 4. **Forward-only filter.** Drop entries from `TablesRemoved`,
971
+ `ColumnsRemoved`, and `ColumnsModified` (where the modification
972
+ isn't purely a default-value change) from the diff before
973
+ generation. Forward-only is enforced at the diff layer, not by
974
+ per-engine DDL guards. Log skipped entries as warnings so an
975
+ operator who actually wants a breaking change knows they need to
976
+ issue it out-of-band.
977
+ 5. **`generateMigrationStatements(filteredDiff, engineType)`** →
978
+ array of DDL statements.
979
+ 6. **Execute via the connector's schemaProvider.** For ADD-only diffs
980
+ we can also short-circuit through `SchemaDeployer.deployTable`
981
+ when an entire new table appears; otherwise execute the statements
982
+ in order.
983
+ 7. **Return** the diff + statement list in the EnsureSchema response
984
+ (currently returns only `{Tables, Indices, Notes}`; gain
985
+ `MigrationStatements: [...]` so operators can see what ran).
986
+
987
+ Net effect:
988
+
989
+ - **Per-engine ADD COLUMN comes free.** `MigrationGenerator` already
990
+ knows about SQLite / MySQL / MSSQL / PostgreSQL DDL idioms; we
991
+ delete the SQLite-only `_alterTablesIfChanged` body Session 2
992
+ shipped.
993
+ - **Schema versioning isn't needed.** The database is the source of
994
+ truth; introspect-then-diff replaces version tracking. The
995
+ `Version: 1` field in `UltravisorPersistenceSchema.json` becomes
996
+ documentation only.
997
+ - **Breaking-change story is "issue the DDL out-of-band, then re-run
998
+ EnsureSchema"** for now. The migration manager has the
999
+ vocabulary for renames / type changes when we want them; we just
1000
+ don't use it on the EnsureSchema path because we don't ship destructive
1001
+ changes automatically.
1002
+
1003
+ #### Embed shape inside DataBeacon-SchemaManager
1004
+
1005
+ The `Retold-Data-Service-MigrationManager.js` wrapper is more than we
1006
+ need (it stands up REST routes, a web UI, DDL file scanning). We
1007
+ borrow the *embed pattern* but instantiate only the services we
1008
+ actually use:
1009
+
1010
+ ```javascript
1011
+ // Inside DataBeacon-SchemaManager.js constructor:
1012
+ const libMeadowMigrationManager = require('meadow-migrationmanager');
1013
+
1014
+ this._MM = new libMeadowMigrationManager(
1015
+ {
1016
+ Product: 'DataBeacon-SchemaManager',
1017
+ LogStreams: pFable.settings.LogStreams || [{ streamtype: 'console' }]
1018
+ });
1019
+ this._SchemaIntrospector = this._MM.instantiateServiceProvider('SchemaIntrospector');
1020
+ this._SchemaDiff = this._MM.instantiateServiceProvider('SchemaDiff');
1021
+ this._MigrationGenerator = this._MM.instantiateServiceProvider('MigrationGenerator');
1022
+ this._SchemaDeployer = this._MM.instantiateServiceProvider('SchemaDeployer');
1023
+ ```
1024
+
1025
+ The existing `_BeaconConnections` cache + connection-routing in
1026
+ SchemaManager stays unchanged; only the bootstrap body changes.
1027
+
1028
+ `package.json` adds `meadow-migrationmanager` as a runtime dep.
1029
+ `stricture` is already there. The four meadow connectors are already
1030
+ there. The TUI / pict-section-flow / pict-terminalui / orator deps
1031
+ inside `meadow-migrationmanager` only load if their services are
1032
+ instantiated; the four services we use don't reach them.
1033
+
1034
+ #### Docker-driven lab smoke
1035
+
1036
+ Session 3's `Persistence_Lab_Smoke_tests.js` uses in-process stub HTTP
1037
+ servers for the UV and databeacon — fast, runs in CI without Docker.
1038
+ Session 4 ships a sibling `Persistence_Lab_Docker_Smoke_tests.js` that
1039
+ exercises the full Docker-spawned chain:
1040
+
1041
+ 1. `before`: skip the suite if Docker isn't reachable (`docker info`
1042
+ probe). Otherwise build the lab images if missing.
1043
+ 2. Spin up a real `retold-databeacon` container via the lab's
1044
+ `Service-BeaconContainerManager.spawn`. Wait for `/beacon/connections`
1045
+ to respond.
1046
+ 3. Spin up a real ultravisor container via
1047
+ `Service-UltravisorManager.startInstance`. Wait for `/status`.
1048
+ 4. POST `/beacon/connection` to the databeacon to add an external
1049
+ SQLite connection pointing at a host-mounted file.
1050
+ 5. POST `/api/lab/ultravisor-instances/:id/persistence-beacon` with the
1051
+ spawned databeacon's `IDBeacon` and the new connection's
1052
+ `IDBeaconConnection`.
1053
+ 6. Poll `/api/lab/ultravisor-instances/:id/persistence-status` until
1054
+ `Persistence.State === 'bootstrapped'` (timeout 60s — image pulls +
1055
+ container cold-start add latency).
1056
+ 7. Trigger a no-op operation through the UV's `/Operation/<hash>/Execute/Async`
1057
+ path. Reuse whatever the existing manifest tests use as the no-op
1058
+ fixture.
1059
+ 8. Verify rows in the host-mounted SQLite via direct better-sqlite3
1060
+ reads (same pattern as Session 2 / Session 3 stubs).
1061
+ 9. Clear the assignment via POST with `IDBeacon: null`. Assert the
1062
+ pill flips to `unassigned`.
1063
+ 10. Teardown: stop both containers, prune.
1064
+
1065
+ The Session 3 stub smoke stays as the Docker-free integration test
1066
+ (runs in CI, stays fast). The Docker smoke is opt-in via env var
1067
+ (`SMOKE_DOCKER=1 npm test` — pattern matches the existing
1068
+ `test-browser` opt-in for puppeteer tests).
1069
+
1070
+ Once the Docker smoke is green, also exercise it against MySQL and
1071
+ Postgres engines (using the lab's existing engine spawning) so the
1072
+ ADD COLUMN generalization (#1 above) gets end-to-end coverage in the
1073
+ same harness.
1074
+
1075
+ #### Bootstrap-flush idempotency on appendEvent
1076
+
1077
+ Open question 4 made it onto Session 4. The `UVQueueWorkItemEvent.EventGUID`
1078
+ column is unique; bootstrap-flush re-pushes events on every reconnect.
1079
+ Today a `409` from the meadow REST endpoint surfaces as
1080
+ `{Available: true, Success: false, Status: 409}` — the flush sweep
1081
+ treats that as a hard failure and aborts.
1082
+
1083
+ Fix: extend `_normalizeMeadowProxyResult` in
1084
+ `Ultravisor-QueuePersistenceBridge.cjs` with action-specific logic for
1085
+ `QP_AppendEvent` (and `QP_InsertAttempt`, which has a similar
1086
+ `(WorkItemHash, AttemptNumber)` unique constraint). Map a 409 (or
1087
+ 500-with-unique-violation, depending on engine) to
1088
+ `{Available: true, Success: true, AlreadyPresent: true}`. Other
1089
+ actions still treat 409 as an error.
1090
+
1091
+ Ensure the flush-sweep loop in
1092
+ `Ultravisor-QueuePersistenceBridge._flushQueueToBeacon` advances the
1093
+ HWM on `AlreadyPresent: true` results — the row is already on the
1094
+ beacon, so the HWM should march forward.
1095
+
1096
+ #### Read-shape normalization audit (closes Open question 3)
1097
+
1098
+ `QP_ListWorkItems` / `QP_GetEvents` / `MS_ListManifests` go through
1099
+ MeadowProxy → meadow's bulk-read endpoint, which returns a bare array.
1100
+ Session 2's `_normalizeMeadowProxyResult` already wraps the array into
1101
+ the `{Available, Success, WorkItems: [...]}` shape `_readOrLocal`'s
1102
+ callers expect, but the wrapping is duplicated across the queue and
1103
+ manifest bridges.
1104
+
1105
+ Plan:
1106
+
1107
+ 1. Audit every `_readOrLocal` consumer (mostly in
1108
+ `Ultravisor-API-Server.cjs`'s `/Manifest`, `/Beacon/Work/...`, and
1109
+ the coordinator's listing paths). Confirm each one consumes the
1110
+ wrapped shape and not the raw beacon response.
1111
+ 2. Pull the array-wrapping logic into a shared helper
1112
+ (`_arrayResult(pAction, pParsed, pSuccess, pListKey)`) on each
1113
+ bridge, replacing the duplicated switch-case branches.
1114
+ 3. Add coverage in the Session 2 bridge smoke for the list + filter
1115
+ paths against MeadowProxy mode (currently only the GET-by-hash
1116
+ path is exercised end-to-end).
1117
+
1118
+ Tangential to Session 4's main thrust but cheap to land alongside the
1119
+ engine-coverage work — same files get touched.
1120
+
1121
+ #### Legacy beacon deprecation
1122
+
1123
+ `ultravisor-queue-beacon` and `ultravisor-manifest-beacon` are still
1124
+ selectable in the lab's beacon-create form. They stay as-is in the
1125
+ codebase (they're the reference Provider implementations and useful for
1126
+ embedded deployments that don't want retold-databeacon's REST surface).
1127
+ Session 4 just makes the recommended path obvious to operators:
1128
+
1129
+ - `Service-BeaconTypeRegistry.js` — append `(legacy)` to the
1130
+ `DisplayName` of both types and add a `Deprecated: true` field on
1131
+ the public descriptor.
1132
+ - `PictView-Lab-Beacons.js` — when the form's BeaconType dropdown
1133
+ shows a deprecated type, render a tooltip / inline note: "Legacy
1134
+ type. New deployments should use `retold-databeacon` + the lab's
1135
+ Persistence assignment for queue / manifest persistence."
1136
+ - The seed-dataset and other paths that filter by `BeaconType ===
1137
+ 'retold-databeacon'` already do the right thing; no other changes.
1138
+
1139
+ #### Test-fable cleanup (tangential hygiene)
1140
+
1141
+ Session 3 verification surfaced 6 pre-existing failures in
1142
+ `modules/apps/ultravisor/test/Ultravisor_BeaconQueue_tests.js` and
1143
+ `Ultravisor_tests.js`:
1144
+
1145
+ - **Coordinator integration tests** — `enqueueWorkItem populates new
1146
+ fields and persists to store` and `dispatch tick promotes Queued
1147
+ items to Dispatched`. Both fail because `buildFable()` doesn't
1148
+ register `UltravisorQueuePersistenceBridge`, so the coordinator's
1149
+ `_getQueuePersistenceBridge()` returns null and persistence is
1150
+ skipped. Fix: register the bridge service alongside the existing
1151
+ `UltravisorBeaconQueueStore` / `UltravisorBeaconCoordinator` /
1152
+ `UltravisorBeaconScheduler` registrations.
1153
+ - **TaskTypeRegistry count tests** — three tests asserting
1154
+ `registry.size === 56` while the registry now has 57. Fix: bump the
1155
+ expected count, or better, derive it from the config array so the
1156
+ next addition doesn't break the test.
1157
+
1158
+ Tangential to the persistence refactor but lands cleanly in Session 4
1159
+ since it's pure hygiene work.
1160
+
1161
+ #### Concrete starting steps
1162
+
1163
+ 1. **Embed `meadow-migrationmanager` in `DataBeacon-SchemaManager`.**
1164
+ Add the dep, instantiate the four services
1165
+ (`SchemaIntrospector` / `SchemaDiff` / `MigrationGenerator` /
1166
+ `SchemaDeployer`) on an isolated MM Pict context. Replace the
1167
+ Session 2 SQLite-only `_alterTablesIfChanged` body with the
1168
+ introspect → diff → forward-only filter → generate → execute
1169
+ pipeline. Keep the existing descriptor translator that produces
1170
+ the shape SchemaDiff expects.
1171
+ 2. **Per-engine integration tests.** Extend
1172
+ `DataBeacon-SchemaManager_tests.js` (the Session 1 / 2 unit
1173
+ coverage) with Docker-spawned MySQL / MSSQL / PostgreSQL cases
1174
+ that prove fresh-bootstrap + incremental ADD COLUMN both work
1175
+ end-to-end. Reuse the connector test suites' existing Docker
1176
+ helpers.
1177
+ 3. **Bootstrap-flush idempotency.** Map 409 / unique-violation to
1178
+ `Success: true, AlreadyPresent: true` for `QP_AppendEvent` and
1179
+ `QP_InsertAttempt`. Advance the HWM on `AlreadyPresent`.
1180
+ 4. **Read-shape normalization.** Pull array-wrapping logic into
1181
+ `_arrayResult` on both bridges; audit consumers.
1182
+ 5. **Docker-driven lab smoke.** New
1183
+ `Persistence_Lab_Docker_Smoke_tests.js` — opt-in via `SMOKE_DOCKER=1`.
1184
+ Skip cleanly when Docker isn't reachable. Run against SQLite +
1185
+ MySQL + Postgres via the lab's existing engine spawning.
1186
+ 6. **Legacy beacon deprecation labels.** `(legacy)` suffix in
1187
+ BeaconTypeRegistry + tooltip in the beacon form.
1188
+ 7. **Test-fable cleanup.** Register the persistence bridge in
1189
+ `Ultravisor_BeaconQueue_tests.js`'s `buildFable`; derive
1190
+ TaskTypeRegistry count from the config array.
1191
+
1192
+ #### Items deferred past Session 4
1193
+
1194
+ - **Real session-user `RemoteUser` threading.** Today's bridges send
1195
+ the synthetic `'ultravisor-system'`. Wiring the originating session
1196
+ user through the
1197
+ `/Ultravisor/Persistence/*` → bridge → MeadowProxy audit path
1198
+ requires threading a context arg through the bridge dispatch API
1199
+ (currently fire-and-forget). Lands when audit-log fidelity becomes a
1200
+ customer-facing requirement.
1201
+ - **Multi-UV deployment topology** (Open question 2). Today's `UV*`
1202
+ table names imply a single-UV-per-databeacon convention; supporting
1203
+ N UVs against the same databeacon needs either per-UV table prefixes
1204
+ (`UV_<HubID>_QueueWorkItem`) or a `UltravisorInstanceID` discriminator
1205
+ column on every row. Defer until multi-UV deployments are concrete
1206
+ enough to choose between the two.
1207
+ - **Hard-delete retention sweep.** Soft-deleted manifests (and
1208
+ cancelled work items, once we add a TTL) accumulate forever today.
1209
+ A periodic retention sweep with a configurable `RetentionDays` per
1210
+ UV is feature work, not refactor cleanup.
1211
+ - **Cross-table queries** (Open question 5). With both queue + manifest
1212
+ in one database, joins like "show me all events for runs of operation
1213
+ X" are now possible at the SQL level but the bridges expose only
1214
+ single-table operations. A future `MeadowProxy.Query` (raw SQL) or
1215
+ `DataBeaconManagement.Join` capability is feature work, not part of
1216
+ the refactor.
1217
+
1218
+ ## Files this work touches (reference)
1219
+
1220
+ ### retold-databeacon
1221
+ - `source/services/DataBeacon-BeaconProvider.js` — register the
1222
+ `DataBeaconSchema` capability alongside the existing three.
1223
+ - `source/services/DataBeacon-SchemaManager.js` — Session 4 reshapes
1224
+ the EnsureSchema body around `meadow-migrationmanager`'s
1225
+ `SchemaIntrospector` + `SchemaDiff` + `MigrationGenerator` +
1226
+ `SchemaDeployer` services. The Session 2 inline descriptor translator
1227
+ stays as a thin adapter; the SQLite-only `_alterTablesIfChanged`
1228
+ body goes away.
1229
+ - `source/services/DataBeacon-MeadowProxyProvider.js` — accept a
1230
+ `PathAllowlist` config update at runtime.
1231
+ - `package.json` — Session 4 adds `meadow-migrationmanager` as a runtime
1232
+ dep. `stricture` is already present (Session 1).
1233
+
1234
+ ### ultravisor
1235
+ - `source/persistence/UltravisorPersistenceSchema.json` (new) —
1236
+ the schema source-of-truth.
1237
+ - `source/services/Ultravisor-QueuePersistenceBridge.cjs` —
1238
+ add `_dispatchViaMeadowProxy`, schema-bootstrap state machine.
1239
+ - `source/services/Ultravisor-ManifestStoreBridge.cjs` — same.
1240
+ - `source/services/Ultravisor-Beacon-Coordinator.cjs` — already calls
1241
+ `_notifyPersistenceBridgesOnConnect` from bootstrap-flush; the bridges'
1242
+ internal logic just grows new branches. No coordinator changes expected.
1243
+
1244
+ ### ultravisor-lab
1245
+ - `source/services/Service-UltravisorManager.js` — add `IDPersistenceBeacon`
1246
+ field to the row schema + getter/setter.
1247
+ - `source/web_server/routes/Lab-Api-Ultravisor.js` — assignment endpoint.
1248
+ - `source/browser_bundle/views/PictView-Lab-Ultravisor.js` — picker UI.
1249
+ - `source/browser_bundle/Lab-Browser-Application.js` —
1250
+ `setPersistenceBeacon(pUvID, pBeaconID)` action.
1251
+ - `source/browser_bundle/providers/PictRouter-Lab-Configuration.json` —
1252
+ route for the picker change.
1253
+
1254
+ ## Open questions for future sessions
1255
+
1256
+ 1. **PathAllowlist push mechanism.** Today retold-databeacon takes the
1257
+ allowlist as a constructor option. To update it at runtime we either
1258
+ (a) restart the beacon, or (b) add a new `DataBeaconManagement.UpdateConfig`
1259
+ action that the BeaconProvider forwards into the MeadowProxy options.
1260
+ Probably (b) — but flag this for the implementer.
1261
+
1262
+ 2. **Shared persistence database vs separate.** When multiple UVs assign
1263
+ the same databeacon, do their tables collide? Right now the schema uses
1264
+ `UV*` prefixes which would mean all UVs share one set of tables. That's
1265
+ wrong if UVs are supposed to be isolated. Two options:
1266
+ - Per-UV table prefix (e.g. `UV_<UltravisorInstanceHash>_QueueWorkItem`).
1267
+ - Add a `UltravisorInstanceID` column on every row.
1268
+ Decision deferred — easier to make once we're closer to multi-UV
1269
+ deployments. For now: single-UV mode only.
1270
+
1271
+ 3. **Read shape on `getEvents` / `listWorkItems`.** Meadow's bulk-read REST
1272
+ surface returns an array directly, not the `{Success, ...}` envelope the
1273
+ bridge's `_readOrLocal` expects. The MeadowProxy translation will need to
1274
+ normalize. Trivial but worth flagging.
1275
+
1276
+ 4. **Idempotency of `appendEvent`.** Bootstrap-flush re-pushes events on
1277
+ reconnect. The schema's `EventGUID` unique constraint catches duplicates,
1278
+ but the bridge needs to handle the resulting `409` / unique-violation
1279
+ gracefully (treat as success, not error).
1280
+
1281
+ 5. **Cross-table queries.** With both queue + manifest in one database,
1282
+ we can write `SELECT ... FROM UVQueueWorkItem JOIN UVManifest ...` for
1283
+ things like "show me all events for runs of operation X". The current
1284
+ bridges expose only single-table operations. A future capability could
1285
+ add `MeadowProxy.Query` (raw SQL) or `DataBeaconManagement.Join` — but
1286
+ that's a feature, not a refactor item.
1287
+
1288
+ 6. **Auth between ultravisor and the assigned databeacon.** Partially
1289
+ addressed in Session 3: the bridges now thread a synthetic
1290
+ `'ultravisor-system'` value through `RemoteUser` on every dispatched
1291
+ `MeadowProxy.Request`, surfacing in MeadowProxy's audit trail. The
1292
+ real session-user pass-through (Secure-mode operator → UV
1293
+ `/Ultravisor/Persistence/*` → bridge → databeacon audit log) is still
1294
+ deferred — `_resolveRemoteUser()` is the single hook to wire it
1295
+ through once the UV API server starts threading the resolved
1296
+ session user into bridge dispatches.
1297
+
1298
+ ## Glossary
1299
+
1300
+ - **bootstrap-flush.** The mechanism (already shipped) that replays locally-
1301
+ buffered writes into a freshly-connected persistence beacon. See the
1302
+ per-bridge `_FlushHWMs` state and `<DataPath>/persistence-bridge-hwm.json`.
1303
+ - **HWM.** High-water mark — the timestamp of the most recent item
1304
+ successfully pushed to a particular beacon. Per-beacon, persisted to disk.
1305
+ - **MeadowProxy.** Capability on retold-databeacon that proxies HTTP requests
1306
+ to its localhost meadow REST API. The mesh-callable equivalent of "make
1307
+ any meadow REST call against this database."
1308
+ - **Persistence beacon.** A retold-databeacon instance assigned via the lab
1309
+ UI to be a specific UV's persistence backend. Same software, contextual
1310
+ role.
1311
+ - **EventGUID.** UUID v4 stamped on every queue/manifest event by ultravisor.
1312
+ Stable across process restarts. The dedup key for replay; Seq is just an
1313
+ ordering hint.