@dsh-enhanced/preference-learning 0.1.7 → 0.1.12

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.
package/lib/sqlite.js CHANGED
@@ -1,7 +1,8 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import { chmodSync, closeSync, constants, lstatSync, mkdirSync, openSync, } from 'node:fs';
2
3
  import { dirname, isAbsolute } from 'node:path';
3
4
  import { DatabaseSync } from 'node:sqlite';
4
- export const preferenceSchemaVersion = 1;
5
+ export const preferenceSchemaVersion = 12;
5
6
  export class PreferenceDatabaseError extends Error {
6
7
  code;
7
8
  constructor(code, message) {
@@ -38,6 +39,20 @@ function preparePrivateFile(path) {
38
39
  function userVersion(database) {
39
40
  return database.prepare('PRAGMA user_version').get().user_version;
40
41
  }
42
+ /** Opaque stable binding used after cancellation rows redact their owner lineage. */
43
+ export function preferencePromotionCancellationUpgradeBindingDigest(input) {
44
+ return createHash('sha256')
45
+ .update('preference-memory-promotion-cancellation-upgrade/v1\0')
46
+ .update(JSON.stringify([
47
+ input.promotionId,
48
+ input.promotionGeneration,
49
+ input.requestDigest,
50
+ input.principalLineageId,
51
+ input.principalLineageVersion,
52
+ input.ownerGeneration,
53
+ ]))
54
+ .digest('hex');
55
+ }
41
56
  function migrate(database) {
42
57
  let transactionStarted = false;
43
58
  try {
@@ -54,7 +69,8 @@ function migrate(database) {
54
69
  transactionStarted = false;
55
70
  return;
56
71
  }
57
- database.exec(`
72
+ if (current < 1)
73
+ database.exec(`
58
74
  CREATE TABLE preference_schema_meta (
59
75
  key TEXT PRIMARY KEY,
60
76
  value TEXT NOT NULL
@@ -153,6 +169,631 @@ function migrate(database) {
153
169
  INSERT INTO preference_schema_meta(key, value) VALUES ('schema-version', '1');
154
170
  PRAGMA user_version = 1;
155
171
  `);
172
+ if (current < 2)
173
+ database.exec(`
174
+ CREATE TABLE preference_host_maintenance_receipts (
175
+ idempotency_key TEXT PRIMARY KEY,
176
+ payload_hash TEXT NOT NULL,
177
+ scope_digest TEXT NOT NULL,
178
+ deleted_signals INTEGER NOT NULL CHECK (deleted_signals IN (0, 1)),
179
+ occurred_at INTEGER NOT NULL
180
+ ) STRICT;
181
+ CREATE INDEX preference_host_maintenance_scope_time
182
+ ON preference_host_maintenance_receipts(scope_digest, occurred_at DESC, idempotency_key);
183
+ UPDATE preference_schema_meta SET value = '2' WHERE key = 'schema-version';
184
+ PRAGMA user_version = 2;
185
+ `);
186
+ if (current < 3)
187
+ database.exec(`
188
+ CREATE TABLE preference_host_activation_receipts (
189
+ idempotency_key TEXT PRIMARY KEY,
190
+ payload_hash TEXT NOT NULL,
191
+ scope_digest TEXT NOT NULL,
192
+ hypothesis_id TEXT NOT NULL,
193
+ expected_version INTEGER NOT NULL CHECK (expected_version >= 1),
194
+ result_version INTEGER NOT NULL CHECK (result_version = expected_version + 1),
195
+ occurred_at INTEGER NOT NULL
196
+ ) STRICT;
197
+ CREATE INDEX preference_host_activation_scope_time
198
+ ON preference_host_activation_receipts(scope_digest, occurred_at DESC, idempotency_key);
199
+ UPDATE preference_schema_meta SET value = '3' WHERE key = 'schema-version';
200
+ PRAGMA user_version = 3;
201
+ `);
202
+ if (current < 4)
203
+ database.exec(`
204
+ CREATE TABLE preference_exposures (
205
+ idempotency_key TEXT PRIMARY KEY,
206
+ payload_hash TEXT NOT NULL,
207
+ scope_key TEXT NOT NULL,
208
+ workspace TEXT NOT NULL,
209
+ preset TEXT NOT NULL,
210
+ hypothesis_id TEXT NOT NULL,
211
+ hypothesis_version INTEGER NOT NULL CHECK (hypothesis_version >= 1),
212
+ session_id TEXT NOT NULL,
213
+ source_event_id TEXT NOT NULL,
214
+ source_inbox_id TEXT,
215
+ reply_outbox_id TEXT,
216
+ state TEXT NOT NULL CHECK (state IN ('pending', 'bound')),
217
+ exposed_at INTEGER NOT NULL,
218
+ bound_at INTEGER,
219
+ expires_at INTEGER NOT NULL,
220
+ CHECK ((state = 'pending' AND source_inbox_id IS NULL AND reply_outbox_id IS NULL AND bound_at IS NULL)
221
+ OR (state = 'bound' AND source_inbox_id IS NOT NULL AND reply_outbox_id IS NOT NULL AND bound_at IS NOT NULL))
222
+ ) STRICT;
223
+ CREATE INDEX preference_exposures_turn
224
+ ON preference_exposures(scope_key, session_id, source_event_id, state, hypothesis_id);
225
+ CREATE INDEX preference_exposures_outbox
226
+ ON preference_exposures(scope_key, reply_outbox_id, state, hypothesis_id);
227
+ CREATE INDEX preference_exposures_expiry
228
+ ON preference_exposures(expires_at, idempotency_key);
229
+ CREATE TABLE preference_exposure_corrections (
230
+ signal_id TEXT NOT NULL,
231
+ hypothesis_id TEXT NOT NULL,
232
+ hypothesis_version INTEGER NOT NULL CHECK (hypothesis_version >= 1),
233
+ source_inbox_id TEXT NOT NULL,
234
+ reply_outbox_id TEXT NOT NULL,
235
+ occurred_at INTEGER NOT NULL,
236
+ PRIMARY KEY (signal_id, hypothesis_id, hypothesis_version)
237
+ ) STRICT;
238
+ CREATE INDEX preference_exposure_corrections_target
239
+ ON preference_exposure_corrections(reply_outbox_id, hypothesis_id, occurred_at);
240
+ UPDATE preference_schema_meta SET value = '4' WHERE key = 'schema-version';
241
+ PRAGMA user_version = 4;
242
+ `);
243
+ if (current < 5)
244
+ database.exec(`
245
+ CREATE TABLE preference_scope_principals (
246
+ scope_key TEXT PRIMARY KEY,
247
+ scope_digest TEXT NOT NULL,
248
+ principal_digest TEXT NOT NULL CHECK (
249
+ length(principal_digest) = 64 AND principal_digest NOT GLOB '*[^a-f0-9]*'
250
+ ),
251
+ generation INTEGER NOT NULL CHECK (generation >= 1),
252
+ claimed_at INTEGER NOT NULL,
253
+ purge_pending INTEGER NOT NULL CHECK (purge_pending IN (0, 1)),
254
+ updated_at INTEGER NOT NULL
255
+ ) STRICT;
256
+ CREATE INDEX preference_scope_principals_digest
257
+ ON preference_scope_principals(scope_digest, principal_digest);
258
+ UPDATE preference_schema_meta SET value = '5' WHERE key = 'schema-version';
259
+ PRAGMA user_version = 5;
260
+ `);
261
+ if (current < 6)
262
+ database.exec(`
263
+ ALTER TABLE preference_scope_principals
264
+ ADD COLUMN principal_lineage_id TEXT;
265
+ ALTER TABLE preference_scope_principals
266
+ ADD COLUMN principal_lineage_version INTEGER;
267
+ ALTER TABLE preference_scope_principals
268
+ ADD COLUMN learning_paused INTEGER NOT NULL DEFAULT 0
269
+ CHECK (learning_paused IN (0, 1));
270
+ ALTER TABLE preference_scope_principals
271
+ ADD COLUMN paused_at INTEGER;
272
+ ALTER TABLE preference_scope_principals
273
+ ADD COLUMN control_version INTEGER NOT NULL DEFAULT 1 CHECK (control_version >= 1);
274
+ ALTER TABLE preference_scope_principals
275
+ ADD COLUMN ignore_events_through INTEGER NOT NULL DEFAULT -1 CHECK (ignore_events_through >= -1);
276
+ ALTER TABLE preference_host_activation_receipts
277
+ ADD COLUMN owner_generation INTEGER;
278
+ ALTER TABLE preference_host_activation_receipts
279
+ ADD COLUMN principal_lineage_id TEXT;
280
+ ALTER TABLE preference_host_activation_receipts
281
+ ADD COLUMN principal_lineage_version INTEGER;
282
+ ALTER TABLE preference_host_maintenance_receipts
283
+ ADD COLUMN owner_generation INTEGER;
284
+ ALTER TABLE preference_host_maintenance_receipts
285
+ ADD COLUMN principal_lineage_id TEXT;
286
+ ALTER TABLE preference_host_maintenance_receipts
287
+ ADD COLUMN principal_lineage_version INTEGER;
288
+ CREATE TABLE preference_owner_control_receipts (
289
+ idempotency_key TEXT PRIMARY KEY,
290
+ payload_hash TEXT NOT NULL,
291
+ scope_digest TEXT NOT NULL,
292
+ principal_digest TEXT NOT NULL CHECK (
293
+ length(principal_digest) = 64 AND principal_digest NOT GLOB '*[^a-f0-9]*'
294
+ ),
295
+ generation INTEGER NOT NULL CHECK (generation >= 1),
296
+ action TEXT NOT NULL CHECK (action IN ('pause', 'resume')),
297
+ result_paused INTEGER NOT NULL CHECK (result_paused IN (0, 1)),
298
+ result_control_version INTEGER NOT NULL CHECK (result_control_version >= 1),
299
+ result_ignore_events_through INTEGER NOT NULL CHECK (result_ignore_events_through >= -1),
300
+ occurred_at INTEGER NOT NULL
301
+ ) STRICT;
302
+ CREATE INDEX preference_owner_control_scope_time
303
+ ON preference_owner_control_receipts(scope_digest, occurred_at DESC, idempotency_key);
304
+ UPDATE preference_schema_meta SET value = '6' WHERE key = 'schema-version';
305
+ PRAGMA user_version = 6;
306
+ `);
307
+ if (current < 7)
308
+ database.exec(`
309
+ ALTER TABLE preference_scope_principals
310
+ ADD COLUMN admission_cursor_epoch TEXT;
311
+ ALTER TABLE preference_scope_principals
312
+ ADD COLUMN lineage_claim_sequence INTEGER
313
+ CHECK (lineage_claim_sequence IS NULL OR lineage_claim_sequence >= 1);
314
+ ALTER TABLE preference_scope_principals
315
+ ADD COLUMN admission_high_water INTEGER
316
+ CHECK (admission_high_water IS NULL OR admission_high_water >= 1);
317
+ ALTER TABLE preference_scope_principals
318
+ ADD COLUMN admission_high_water_kind TEXT
319
+ CHECK (admission_high_water_kind IN ('control', 'event'));
320
+ ALTER TABLE preference_scope_principals
321
+ ADD COLUMN ignore_events_through_sequence INTEGER
322
+ CHECK (ignore_events_through_sequence IS NULL OR ignore_events_through_sequence >= 1);
323
+
324
+ -- v6 control receipts were ordered only by millisecond timestamps and
325
+ -- cannot safely participate in the v7 total order. Drop them instead of
326
+ -- inventing cursors during a rolling upgrade.
327
+ DROP TABLE preference_owner_control_receipts;
328
+ CREATE TABLE preference_owner_control_receipts (
329
+ idempotency_key TEXT PRIMARY KEY,
330
+ payload_hash TEXT NOT NULL,
331
+ scope_digest TEXT NOT NULL,
332
+ principal_digest TEXT NOT NULL CHECK (
333
+ length(principal_digest) = 64 AND principal_digest NOT GLOB '*[^a-f0-9]*'
334
+ ),
335
+ generation INTEGER NOT NULL CHECK (generation >= 1),
336
+ action TEXT NOT NULL CHECK (action IN ('forget', 'pause', 'resume', 'status')),
337
+ admission_cursor_epoch TEXT NOT NULL,
338
+ admission_cursor_sequence INTEGER NOT NULL CHECK (admission_cursor_sequence >= 1),
339
+ result_applied INTEGER NOT NULL CHECK (result_applied IN (0, 1)),
340
+ result_paused INTEGER NOT NULL CHECK (result_paused IN (0, 1)),
341
+ result_control_version INTEGER NOT NULL CHECK (result_control_version >= 1),
342
+ result_admission_high_water INTEGER
343
+ CHECK (result_admission_high_water IS NULL OR result_admission_high_water >= 1),
344
+ result_ignore_events_through_sequence INTEGER
345
+ CHECK (result_ignore_events_through_sequence IS NULL
346
+ OR result_ignore_events_through_sequence >= 1),
347
+ result_signals INTEGER NOT NULL CHECK (result_signals >= 0),
348
+ result_hypotheses INTEGER NOT NULL CHECK (result_hypotheses >= 0),
349
+ result_active_overlays INTEGER NOT NULL CHECK (result_active_overlays >= 0),
350
+ result_shadow_hypotheses INTEGER NOT NULL CHECK (result_shadow_hypotheses >= 0),
351
+ result_deleted_signals INTEGER NOT NULL DEFAULT 0 CHECK (result_deleted_signals >= 0),
352
+ result_deleted_hypotheses INTEGER NOT NULL DEFAULT 0 CHECK (result_deleted_hypotheses >= 0),
353
+ result_forgotten_through INTEGER NOT NULL DEFAULT -1 CHECK (result_forgotten_through >= -1),
354
+ occurred_at INTEGER NOT NULL
355
+ ) STRICT;
356
+ CREATE INDEX preference_owner_control_scope_time
357
+ ON preference_owner_control_receipts(
358
+ scope_digest, admission_cursor_epoch, admission_cursor_sequence DESC, idempotency_key
359
+ );
360
+ UPDATE preference_schema_meta SET value = '7' WHERE key = 'schema-version';
361
+ PRAGMA user_version = 7;
362
+ `);
363
+ if (current < 8)
364
+ database.exec(`
365
+ ALTER TABLE preference_owner_control_receipts
366
+ RENAME TO preference_owner_control_receipts_v7;
367
+ DROP INDEX preference_owner_control_scope_time;
368
+ CREATE TABLE preference_owner_control_receipts (
369
+ idempotency_key TEXT PRIMARY KEY,
370
+ payload_hash TEXT NOT NULL,
371
+ scope_digest TEXT NOT NULL,
372
+ principal_digest TEXT NOT NULL CHECK (
373
+ length(principal_digest) = 64 AND principal_digest NOT GLOB '*[^a-f0-9]*'
374
+ ),
375
+ generation INTEGER NOT NULL CHECK (generation >= 1),
376
+ action TEXT NOT NULL CHECK (action IN (
377
+ 'explain', 'forget', 'pause', 'resume', 'rollback', 'status'
378
+ )),
379
+ target_preference_key TEXT,
380
+ admission_cursor_epoch TEXT NOT NULL,
381
+ admission_cursor_sequence INTEGER NOT NULL CHECK (admission_cursor_sequence >= 1),
382
+ result_applied INTEGER NOT NULL CHECK (result_applied IN (0, 1)),
383
+ result_paused INTEGER NOT NULL CHECK (result_paused IN (0, 1)),
384
+ result_control_version INTEGER NOT NULL CHECK (result_control_version >= 1),
385
+ result_admission_high_water INTEGER
386
+ CHECK (result_admission_high_water IS NULL OR result_admission_high_water >= 1),
387
+ result_ignore_events_through_sequence INTEGER
388
+ CHECK (result_ignore_events_through_sequence IS NULL
389
+ OR result_ignore_events_through_sequence >= 1),
390
+ result_signals INTEGER NOT NULL CHECK (result_signals >= 0),
391
+ result_hypotheses INTEGER NOT NULL CHECK (result_hypotheses >= 0),
392
+ result_active_overlays INTEGER NOT NULL CHECK (result_active_overlays >= 0),
393
+ result_stored_active_overlays INTEGER NOT NULL DEFAULT 0
394
+ CHECK (result_stored_active_overlays >= 0),
395
+ result_shadow_hypotheses INTEGER NOT NULL CHECK (result_shadow_hypotheses >= 0),
396
+ result_deleted_signals INTEGER NOT NULL DEFAULT 0 CHECK (result_deleted_signals >= 0),
397
+ result_deleted_hypotheses INTEGER NOT NULL DEFAULT 0 CHECK (result_deleted_hypotheses >= 0),
398
+ result_forgotten_through INTEGER NOT NULL DEFAULT -1 CHECK (result_forgotten_through >= -1),
399
+ result_explanation_json TEXT,
400
+ result_rolled_back INTEGER NOT NULL DEFAULT 0 CHECK (result_rolled_back IN (0, 1)),
401
+ result_rolled_back_version INTEGER
402
+ CHECK (result_rolled_back_version IS NULL OR result_rolled_back_version >= 2),
403
+ occurred_at INTEGER NOT NULL,
404
+ CHECK ((action = 'rollback' AND target_preference_key IS NOT NULL)
405
+ OR (action != 'rollback' AND target_preference_key IS NULL)),
406
+ CHECK ((action = 'explain' AND result_explanation_json IS NOT NULL)
407
+ OR (action != 'explain' AND result_explanation_json IS NULL)),
408
+ CHECK (result_rolled_back = 0 OR action = 'rollback')
409
+ ) STRICT;
410
+ INSERT INTO preference_owner_control_receipts(
411
+ idempotency_key, payload_hash, scope_digest, principal_digest, generation,
412
+ action, admission_cursor_epoch, admission_cursor_sequence, result_applied,
413
+ result_paused, result_control_version, result_admission_high_water,
414
+ result_ignore_events_through_sequence, result_signals, result_hypotheses,
415
+ result_active_overlays, result_stored_active_overlays, result_shadow_hypotheses,
416
+ result_deleted_signals, result_deleted_hypotheses,
417
+ result_forgotten_through, occurred_at
418
+ )
419
+ SELECT idempotency_key, payload_hash, scope_digest, principal_digest, generation,
420
+ action, admission_cursor_epoch, admission_cursor_sequence, result_applied,
421
+ result_paused, result_control_version, result_admission_high_water,
422
+ result_ignore_events_through_sequence, result_signals, result_hypotheses,
423
+ result_active_overlays, result_active_overlays, result_shadow_hypotheses,
424
+ result_deleted_signals, result_deleted_hypotheses,
425
+ result_forgotten_through, occurred_at
426
+ FROM preference_owner_control_receipts_v7;
427
+ DROP TABLE preference_owner_control_receipts_v7;
428
+ CREATE INDEX preference_owner_control_scope_time
429
+ ON preference_owner_control_receipts(
430
+ scope_digest, admission_cursor_epoch, admission_cursor_sequence DESC, idempotency_key
431
+ );
432
+ UPDATE preference_schema_meta SET value = '8' WHERE key = 'schema-version';
433
+ PRAGMA user_version = 8;
434
+ `);
435
+ if (current < 9)
436
+ database.exec(`
437
+ ALTER TABLE preference_owner_control_receipts
438
+ RENAME TO preference_owner_control_receipts_v8;
439
+ DROP INDEX preference_owner_control_scope_time;
440
+ CREATE TABLE preference_owner_control_receipts (
441
+ idempotency_key TEXT PRIMARY KEY,
442
+ payload_hash TEXT NOT NULL,
443
+ scope_digest TEXT NOT NULL,
444
+ principal_digest TEXT NOT NULL CHECK (
445
+ length(principal_digest) = 64 AND principal_digest NOT GLOB '*[^a-f0-9]*'
446
+ ),
447
+ generation INTEGER NOT NULL CHECK (generation >= 1),
448
+ action TEXT NOT NULL CHECK (action IN (
449
+ 'explain', 'export', 'forget', 'pause', 'resume', 'rollback', 'status'
450
+ )),
451
+ target_preference_key TEXT,
452
+ admission_cursor_epoch TEXT NOT NULL,
453
+ admission_cursor_sequence INTEGER NOT NULL CHECK (admission_cursor_sequence >= 1),
454
+ result_applied INTEGER NOT NULL CHECK (result_applied IN (0, 1)),
455
+ result_paused INTEGER NOT NULL CHECK (result_paused IN (0, 1)),
456
+ result_control_version INTEGER NOT NULL CHECK (result_control_version >= 1),
457
+ result_admission_high_water INTEGER
458
+ CHECK (result_admission_high_water IS NULL OR result_admission_high_water >= 1),
459
+ result_ignore_events_through_sequence INTEGER
460
+ CHECK (result_ignore_events_through_sequence IS NULL
461
+ OR result_ignore_events_through_sequence >= 1),
462
+ result_signals INTEGER NOT NULL CHECK (result_signals >= 0),
463
+ result_hypotheses INTEGER NOT NULL CHECK (result_hypotheses >= 0),
464
+ result_active_overlays INTEGER NOT NULL CHECK (result_active_overlays >= 0),
465
+ result_stored_active_overlays INTEGER NOT NULL DEFAULT 0
466
+ CHECK (result_stored_active_overlays >= 0),
467
+ result_shadow_hypotheses INTEGER NOT NULL CHECK (result_shadow_hypotheses >= 0),
468
+ result_deleted_signals INTEGER NOT NULL DEFAULT 0 CHECK (result_deleted_signals >= 0),
469
+ result_deleted_hypotheses INTEGER NOT NULL DEFAULT 0 CHECK (result_deleted_hypotheses >= 0),
470
+ result_forgotten_through INTEGER NOT NULL DEFAULT -1 CHECK (result_forgotten_through >= -1),
471
+ result_explanation_json TEXT,
472
+ result_rolled_back INTEGER NOT NULL DEFAULT 0 CHECK (result_rolled_back IN (0, 1)),
473
+ result_rolled_back_version INTEGER
474
+ CHECK (result_rolled_back_version IS NULL OR result_rolled_back_version >= 2),
475
+ occurred_at INTEGER NOT NULL,
476
+ CHECK ((action = 'rollback' AND target_preference_key IS NOT NULL)
477
+ OR (action != 'rollback' AND target_preference_key IS NULL)),
478
+ CHECK ((action IN ('explain', 'export') AND result_explanation_json IS NOT NULL)
479
+ OR (action NOT IN ('explain', 'export') AND result_explanation_json IS NULL)),
480
+ CHECK (result_rolled_back = 0 OR action = 'rollback')
481
+ ) STRICT;
482
+ INSERT INTO preference_owner_control_receipts(
483
+ idempotency_key, payload_hash, scope_digest, principal_digest, generation,
484
+ action, target_preference_key, admission_cursor_epoch, admission_cursor_sequence,
485
+ result_applied, result_paused, result_control_version, result_admission_high_water,
486
+ result_ignore_events_through_sequence, result_signals, result_hypotheses,
487
+ result_active_overlays, result_stored_active_overlays, result_shadow_hypotheses,
488
+ result_deleted_signals, result_deleted_hypotheses, result_forgotten_through,
489
+ result_explanation_json, result_rolled_back, result_rolled_back_version, occurred_at
490
+ )
491
+ SELECT idempotency_key, payload_hash, scope_digest, principal_digest, generation,
492
+ action, target_preference_key, admission_cursor_epoch, admission_cursor_sequence,
493
+ result_applied, result_paused, result_control_version, result_admission_high_water,
494
+ result_ignore_events_through_sequence, result_signals, result_hypotheses,
495
+ result_active_overlays, result_stored_active_overlays, result_shadow_hypotheses,
496
+ result_deleted_signals, result_deleted_hypotheses, result_forgotten_through,
497
+ result_explanation_json, result_rolled_back, result_rolled_back_version, occurred_at
498
+ FROM preference_owner_control_receipts_v8;
499
+ DROP TABLE preference_owner_control_receipts_v8;
500
+ CREATE INDEX preference_owner_control_scope_time
501
+ ON preference_owner_control_receipts(
502
+ scope_digest, admission_cursor_epoch, admission_cursor_sequence DESC, idempotency_key
503
+ );
504
+ UPDATE preference_schema_meta SET value = '9' WHERE key = 'schema-version';
505
+ PRAGMA user_version = 9;
506
+ `);
507
+ if (current < 10)
508
+ database.exec(`
509
+ CREATE TABLE preference_memory_promotions (
510
+ promotion_id TEXT NOT NULL,
511
+ promotion_generation INTEGER NOT NULL CHECK (promotion_generation >= 1),
512
+ scope_key TEXT NOT NULL,
513
+ scope_digest TEXT NOT NULL CHECK (
514
+ length(scope_digest) = 64 AND scope_digest NOT GLOB '*[^a-f0-9]*'
515
+ ),
516
+ workspace TEXT NOT NULL,
517
+ preset TEXT NOT NULL,
518
+ principal_id TEXT NOT NULL,
519
+ principal_digest TEXT NOT NULL CHECK (
520
+ length(principal_digest) = 64 AND principal_digest NOT GLOB '*[^a-f0-9]*'
521
+ ),
522
+ principal_lineage_id TEXT NOT NULL,
523
+ principal_lineage_version INTEGER NOT NULL CHECK (principal_lineage_version >= 1),
524
+ owner_generation INTEGER NOT NULL CHECK (owner_generation >= 1),
525
+ hypothesis_id TEXT NOT NULL,
526
+ hypothesis_key TEXT NOT NULL CHECK (hypothesis_key = 'memory.retention'),
527
+ hypothesis_value TEXT NOT NULL CHECK (hypothesis_value = 'long-term'),
528
+ hypothesis_version INTEGER NOT NULL CHECK (hypothesis_version >= 1),
529
+ confidence_bps INTEGER NOT NULL CHECK (confidence_bps BETWEEN 0 AND 10000),
530
+ contradiction_bps INTEGER NOT NULL CHECK (contradiction_bps BETWEEN 0 AND 10000),
531
+ supporting_signals INTEGER NOT NULL CHECK (supporting_signals >= 2),
532
+ distinct_signal_sources INTEGER NOT NULL CHECK (distinct_signal_sources >= 1),
533
+ evidence_mass INTEGER NOT NULL CHECK (evidence_mass >= 1),
534
+ renderer_id TEXT NOT NULL CHECK (renderer_id = 'memory.retention.long-term/v1'),
535
+ observed_at INTEGER NOT NULL CHECK (observed_at >= 0),
536
+ deadline_at INTEGER NOT NULL CHECK (deadline_at > observed_at),
537
+ request_digest TEXT NOT NULL UNIQUE CHECK (
538
+ length(request_digest) = 64 AND request_digest NOT GLOB '*[^a-f0-9]*'
539
+ ),
540
+ idempotency_key TEXT NOT NULL UNIQUE,
541
+ state TEXT NOT NULL CHECK (state IN (
542
+ 'pending', 'submitted', 'confirmed', 'rejected', 'expired',
543
+ 'conflicted', 'stale-owner', 'cancelled'
544
+ )),
545
+ memory_proposal_id TEXT,
546
+ submission_receipt_digest TEXT CHECK (submission_receipt_digest IS NULL OR (
547
+ length(submission_receipt_digest) = 64
548
+ AND submission_receipt_digest NOT GLOB '*[^a-f0-9]*'
549
+ )),
550
+ terminal_result_version INTEGER CHECK (
551
+ terminal_result_version IS NULL OR terminal_result_version >= 1
552
+ ),
553
+ terminal_receipt_digest TEXT CHECK (terminal_receipt_digest IS NULL OR (
554
+ length(terminal_receipt_digest) = 64
555
+ AND terminal_receipt_digest NOT GLOB '*[^a-f0-9]*'
556
+ )),
557
+ created_at INTEGER NOT NULL,
558
+ updated_at INTEGER NOT NULL,
559
+ PRIMARY KEY (promotion_id, promotion_generation),
560
+ UNIQUE(scope_key, hypothesis_id, promotion_generation)
561
+ ) STRICT;
562
+ CREATE INDEX preference_memory_promotions_scope_state
563
+ ON preference_memory_promotions(scope_key, state, updated_at, promotion_id);
564
+ CREATE INDEX preference_memory_promotions_hypothesis
565
+ ON preference_memory_promotions(hypothesis_id, promotion_generation DESC);
566
+
567
+ CREATE TABLE preference_memory_promotion_outbox (
568
+ promotion_id TEXT NOT NULL,
569
+ promotion_generation INTEGER NOT NULL CHECK (promotion_generation >= 1),
570
+ state TEXT NOT NULL CHECK (state IN ('pending', 'retry_wait', 'submitted', 'cancelled')),
571
+ attempt_count INTEGER NOT NULL CHECK (attempt_count >= 0),
572
+ next_attempt_at INTEGER NOT NULL CHECK (next_attempt_at >= 0),
573
+ submitted_at INTEGER,
574
+ last_error TEXT,
575
+ updated_at INTEGER NOT NULL,
576
+ PRIMARY KEY (promotion_id, promotion_generation),
577
+ FOREIGN KEY (promotion_id, promotion_generation)
578
+ REFERENCES preference_memory_promotions(promotion_id, promotion_generation)
579
+ ON DELETE CASCADE
580
+ ) STRICT;
581
+ CREATE INDEX preference_memory_promotion_outbox_pending
582
+ ON preference_memory_promotion_outbox(state, next_attempt_at, updated_at, promotion_id);
583
+
584
+ CREATE TABLE preference_memory_promotion_results (
585
+ promotion_id TEXT NOT NULL,
586
+ promotion_generation INTEGER NOT NULL CHECK (promotion_generation >= 1),
587
+ result_version INTEGER NOT NULL CHECK (result_version >= 1),
588
+ request_digest TEXT NOT NULL CHECK (
589
+ length(request_digest) = 64 AND request_digest NOT GLOB '*[^a-f0-9]*'
590
+ ),
591
+ receipt_digest TEXT NOT NULL UNIQUE CHECK (
592
+ length(receipt_digest) = 64 AND receipt_digest NOT GLOB '*[^a-f0-9]*'
593
+ ),
594
+ status TEXT NOT NULL CHECK (status IN (
595
+ 'confirmed', 'rejected', 'expired', 'conflicted', 'stale-owner'
596
+ )),
597
+ rejection_kind TEXT CHECK (rejection_kind IS NULL OR rejection_kind = 'owner-explicit'),
598
+ memory_proposal_id TEXT,
599
+ memory_proposal_version INTEGER CHECK (
600
+ memory_proposal_version IS NULL OR memory_proposal_version >= 1
601
+ ),
602
+ memory_record_id TEXT,
603
+ memory_record_version INTEGER CHECK (
604
+ memory_record_version IS NULL OR memory_record_version >= 1
605
+ ),
606
+ memory_record_digest TEXT CHECK (memory_record_digest IS NULL OR (
607
+ length(memory_record_digest) = 64
608
+ AND memory_record_digest NOT GLOB '*[^a-f0-9]*'
609
+ )),
610
+ occurred_at INTEGER NOT NULL CHECK (occurred_at >= 0),
611
+ projected_at INTEGER NOT NULL CHECK (projected_at >= 0),
612
+ PRIMARY KEY (promotion_id, promotion_generation),
613
+ FOREIGN KEY (promotion_id, promotion_generation)
614
+ REFERENCES preference_memory_promotions(promotion_id, promotion_generation)
615
+ ON DELETE CASCADE,
616
+ CHECK ((status = 'confirmed'
617
+ AND memory_proposal_id IS NOT NULL AND memory_proposal_version IS NOT NULL
618
+ AND memory_record_id IS NOT NULL AND memory_record_version IS NOT NULL
619
+ AND memory_record_digest IS NOT NULL AND rejection_kind IS NULL)
620
+ OR (status = 'rejected'
621
+ AND memory_proposal_id IS NOT NULL AND memory_proposal_version IS NOT NULL
622
+ AND rejection_kind = 'owner-explicit' AND memory_record_id IS NULL
623
+ AND memory_record_version IS NULL AND memory_record_digest IS NULL)
624
+ OR (status IN ('expired', 'conflicted')
625
+ AND memory_proposal_id IS NOT NULL AND memory_proposal_version IS NOT NULL
626
+ AND rejection_kind IS NULL AND memory_record_id IS NULL
627
+ AND memory_record_version IS NULL AND memory_record_digest IS NULL)
628
+ OR (status = 'stale-owner'
629
+ AND memory_proposal_id IS NULL AND memory_proposal_version IS NULL
630
+ AND rejection_kind IS NULL AND memory_record_id IS NULL
631
+ AND memory_record_version IS NULL AND memory_record_digest IS NULL))
632
+ ) STRICT;
633
+
634
+ -- Privacy-preserving resurrection fence retained after owner rotation or
635
+ -- forget. It contains only opaque/digested promotion identity.
636
+ CREATE TABLE preference_memory_promotion_cancellations (
637
+ promotion_id TEXT NOT NULL,
638
+ promotion_generation INTEGER NOT NULL CHECK (promotion_generation >= 1),
639
+ request_digest TEXT NOT NULL CHECK (
640
+ length(request_digest) = 64 AND request_digest NOT GLOB '*[^a-f0-9]*'
641
+ ),
642
+ principal_lineage_id TEXT NOT NULL,
643
+ principal_lineage_version INTEGER NOT NULL CHECK (principal_lineage_version >= 1),
644
+ owner_generation INTEGER NOT NULL CHECK (owner_generation >= 1),
645
+ reason TEXT NOT NULL CHECK (reason IN (
646
+ 'forget', 'owner-rotated', 'superseded'
647
+ )),
648
+ cancelled_at INTEGER NOT NULL CHECK (cancelled_at >= 0),
649
+ cancellation_digest TEXT NOT NULL UNIQUE CHECK (
650
+ length(cancellation_digest) = 64 AND cancellation_digest NOT GLOB '*[^a-f0-9]*'
651
+ ),
652
+ state TEXT NOT NULL CHECK (state IN (
653
+ 'pending', 'retry_wait', 'cancelled', 'already-confirmed'
654
+ )),
655
+ attempt_count INTEGER NOT NULL CHECK (attempt_count >= 0),
656
+ next_attempt_at INTEGER NOT NULL CHECK (next_attempt_at >= 0),
657
+ receipt_digest TEXT CHECK (receipt_digest IS NULL OR (
658
+ length(receipt_digest) = 64 AND receipt_digest NOT GLOB '*[^a-f0-9]*'
659
+ )),
660
+ last_error TEXT,
661
+ updated_at INTEGER NOT NULL,
662
+ PRIMARY KEY (promotion_id, promotion_generation)
663
+ ) STRICT;
664
+ CREATE INDEX preference_memory_promotion_cancellations_pending
665
+ ON preference_memory_promotion_cancellations(state, next_attempt_at, updated_at, promotion_id);
666
+
667
+ UPDATE preference_schema_meta SET value = '10' WHERE key = 'schema-version';
668
+ PRAGMA user_version = 10;
669
+ `);
670
+ if (current < 11)
671
+ database.exec(`
672
+ UPDATE preference_schema_meta SET value = '11' WHERE key = 'schema-version';
673
+ PRAGMA user_version = 11;
674
+ `);
675
+ if (current < 12) {
676
+ database.exec(`
677
+ ALTER TABLE preference_memory_promotion_cancellations
678
+ ADD COLUMN upgrade_binding_digest TEXT CHECK (
679
+ upgrade_binding_digest IS NULL OR (
680
+ length(upgrade_binding_digest) = 64
681
+ AND upgrade_binding_digest NOT GLOB '*[^a-f0-9]*'
682
+ )
683
+ );
684
+ `);
685
+ const rows = database.prepare(`
686
+ SELECT cancellation.promotion_id, cancellation.promotion_generation,
687
+ cancellation.request_digest, cancellation.principal_lineage_id,
688
+ cancellation.principal_lineage_version, cancellation.owner_generation,
689
+ cancellation.reason, cancellation.state, cancellation.receipt_digest,
690
+ promotion.request_digest AS promotion_request_digest,
691
+ promotion.principal_lineage_id AS promotion_principal_lineage_id,
692
+ promotion.principal_lineage_version AS promotion_principal_lineage_version,
693
+ promotion.owner_generation AS promotion_owner_generation
694
+ FROM preference_memory_promotion_cancellations cancellation
695
+ LEFT JOIN preference_memory_promotions promotion
696
+ ON promotion.promotion_id = cancellation.promotion_id
697
+ AND promotion.promotion_generation = cancellation.promotion_generation
698
+ `).all();
699
+ const update = database.prepare(`
700
+ UPDATE preference_memory_promotion_cancellations
701
+ SET upgrade_binding_digest = ?, principal_lineage_id = ?,
702
+ principal_lineage_version = ?, owner_generation = ?
703
+ WHERE promotion_id = ? AND promotion_generation = ?
704
+ `);
705
+ for (const row of rows) {
706
+ const redacted = row.receipt_digest !== null
707
+ && row.principal_lineage_id.startsWith('redacted-');
708
+ const hasPromotionBinding = row.promotion_request_digest !== null
709
+ && row.promotion_principal_lineage_id !== null
710
+ && row.promotion_principal_lineage_version !== null
711
+ && row.promotion_owner_generation !== null
712
+ && row.promotion_request_digest === row.request_digest;
713
+ const requiresDelivery = row.state === 'pending' || row.state === 'retry_wait'
714
+ || (row.state === 'already-confirmed' && row.reason !== 'superseded');
715
+ if (redacted && !hasPromotionBinding
716
+ && (requiresDelivery || row.reason === 'superseded')) {
717
+ throw new PreferenceDatabaseError('unsafe-file', 'legacy promotion cancellation cannot be safely upgraded after owner-lineage redaction');
718
+ }
719
+ const binding = hasPromotionBinding ? {
720
+ promotionId: row.promotion_id,
721
+ promotionGeneration: row.promotion_generation,
722
+ requestDigest: row.promotion_request_digest,
723
+ principalLineageId: row.promotion_principal_lineage_id,
724
+ principalLineageVersion: row.promotion_principal_lineage_version,
725
+ ownerGeneration: row.promotion_owner_generation,
726
+ } : {
727
+ promotionId: row.promotion_id,
728
+ promotionGeneration: row.promotion_generation,
729
+ requestDigest: row.request_digest,
730
+ principalLineageId: row.principal_lineage_id,
731
+ principalLineageVersion: row.principal_lineage_version,
732
+ ownerGeneration: row.owner_generation,
733
+ };
734
+ const restoreLineage = redacted
735
+ && requiresDelivery
736
+ && hasPromotionBinding;
737
+ update.run(preferencePromotionCancellationUpgradeBindingDigest(binding), restoreLineage ? binding.principalLineageId : row.principal_lineage_id, restoreLineage ? binding.principalLineageVersion : row.principal_lineage_version, restoreLineage ? binding.ownerGeneration : row.owner_generation, row.promotion_id, row.promotion_generation);
738
+ }
739
+ database.exec(`
740
+ ALTER TABLE preference_memory_promotion_cancellations
741
+ RENAME TO preference_memory_promotion_cancellations_v11;
742
+ CREATE TABLE preference_memory_promotion_cancellations (
743
+ promotion_id TEXT NOT NULL,
744
+ promotion_generation INTEGER NOT NULL CHECK (promotion_generation >= 1),
745
+ request_digest TEXT NOT NULL CHECK (
746
+ length(request_digest) = 64 AND request_digest NOT GLOB '*[^a-f0-9]*'
747
+ ),
748
+ principal_lineage_id TEXT NOT NULL,
749
+ principal_lineage_version INTEGER NOT NULL CHECK (principal_lineage_version >= 1),
750
+ owner_generation INTEGER NOT NULL CHECK (owner_generation >= 1),
751
+ reason TEXT NOT NULL CHECK (reason IN (
752
+ 'forget', 'owner-rotated', 'superseded'
753
+ )),
754
+ cancelled_at INTEGER NOT NULL CHECK (cancelled_at >= 0),
755
+ cancellation_digest TEXT NOT NULL UNIQUE CHECK (
756
+ length(cancellation_digest) = 64 AND cancellation_digest NOT GLOB '*[^a-f0-9]*'
757
+ ),
758
+ state TEXT NOT NULL CHECK (state IN (
759
+ 'pending', 'retry_wait', 'cancelled', 'already-confirmed'
760
+ )),
761
+ attempt_count INTEGER NOT NULL CHECK (attempt_count >= 0),
762
+ next_attempt_at INTEGER NOT NULL CHECK (next_attempt_at >= 0),
763
+ receipt_digest TEXT CHECK (receipt_digest IS NULL OR (
764
+ length(receipt_digest) = 64 AND receipt_digest NOT GLOB '*[^a-f0-9]*'
765
+ )),
766
+ last_error TEXT,
767
+ upgrade_binding_digest TEXT NOT NULL CHECK (
768
+ length(upgrade_binding_digest) = 64
769
+ AND upgrade_binding_digest NOT GLOB '*[^a-f0-9]*'
770
+ ),
771
+ updated_at INTEGER NOT NULL,
772
+ PRIMARY KEY (promotion_id, promotion_generation)
773
+ ) STRICT;
774
+ INSERT INTO preference_memory_promotion_cancellations(
775
+ promotion_id, promotion_generation, request_digest, principal_lineage_id,
776
+ principal_lineage_version, owner_generation, reason, cancelled_at,
777
+ cancellation_digest, state, attempt_count, next_attempt_at, receipt_digest,
778
+ last_error, upgrade_binding_digest, updated_at
779
+ )
780
+ SELECT promotion_id, promotion_generation, request_digest, principal_lineage_id,
781
+ principal_lineage_version, owner_generation, reason, cancelled_at,
782
+ cancellation_digest, state, attempt_count, next_attempt_at, receipt_digest,
783
+ last_error, upgrade_binding_digest, updated_at
784
+ FROM preference_memory_promotion_cancellations_v11;
785
+ DROP TABLE preference_memory_promotion_cancellations_v11;
786
+ CREATE INDEX preference_memory_promotion_cancellations_pending
787
+ ON preference_memory_promotion_cancellations(
788
+ state, next_attempt_at, updated_at, promotion_id
789
+ );
790
+ UPDATE preference_memory_promotion_cancellations
791
+ SET state = 'pending', receipt_digest = NULL, last_error = NULL, next_attempt_at = 0
792
+ WHERE state = 'already-confirmed' AND reason IN ('forget', 'owner-rotated');
793
+ UPDATE preference_schema_meta SET value = '12' WHERE key = 'schema-version';
794
+ PRAGMA user_version = 12;
795
+ `);
796
+ }
156
797
  database.exec('COMMIT');
157
798
  transactionStarted = false;
158
799
  }