@dsh-enhanced/preference-learning 0.1.7 → 0.1.14

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/service.js CHANGED
@@ -1,6 +1,9 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
1
2
  import { Context, Service } from '@deepseek-ai/cordis';
2
3
  import Schema from '@deepseek-ai/schemastery';
3
- import { PreferenceStore, canonicalPreferenceScope } from './store.js';
4
+ import { PREFERENCE_MEMORY_PROMOTION_PROTOCOL, brandPreferenceMemoryPromotionProducer, unbrandPreferenceMemoryPromotionProducer, validatePreferenceMemoryPromotionCancellationReceipt, validatePreferenceMemoryPromotionResult, validatePreferenceMemoryPromotionSubmissionReceipt, } from '@dsh-enhanced/assistant-growth-contract';
5
+ import { DELIVERY_PREFERENCE_PROJECTION_PROTOCOL, isTrustedDeliveryPreferenceProducer, } from '@dsh-enhanced/assistant-delivery';
6
+ import { PreferenceStore, PreferenceStoreError, canonicalPreferenceScope } from './store.js';
4
7
  import { registerPreferenceTools } from './tools.js';
5
8
  const configSchema = Schema.object({
6
9
  enabled: Schema.boolean().default(true),
@@ -8,6 +11,8 @@ const configSchema = Schema.object({
8
11
  signalTtlMs: Schema.number().step(1).min(86_400_000).max(31_536_000_000).default(7_776_000_000),
9
12
  hypothesisTtlMs: Schema.number().step(1).min(3_600_000).max(2_592_000_000).default(2_592_000_000),
10
13
  minSignalsForActivation: Schema.number().step(1).min(2).max(20).default(2),
14
+ minBehavioralSignalsForActivation: Schema.number().step(1).min(4).max(20).default(6),
15
+ autonomousT1Enabled: Schema.boolean().default(true),
11
16
  minConfidenceBps: Schema.number().step(1).min(6_000).max(9_500).default(7_500),
12
17
  maxContradictionBps: Schema.number().step(1).min(0).max(4_000).default(2_500),
13
18
  maxActiveOverlays: Schema.number().step(1).min(1).max(8).default(6),
@@ -15,6 +20,7 @@ const configSchema = Schema.object({
15
20
  maxOverlayBytes: Schema.number().step(1).min(256).max(2_048).default(2_048),
16
21
  maintenanceIntervalMs: Schema.number().step(1).min(60_000).max(86_400_000).default(3_600_000),
17
22
  maintenanceBatchSize: Schema.number().step(1).min(1).max(5_000).default(500),
23
+ promotionReconcileLimit: Schema.number().step(1).min(1).max(1_000).default(50),
18
24
  });
19
25
  export class PreferenceLearningError extends Error {
20
26
  code;
@@ -27,15 +33,86 @@ export class PreferenceLearningError extends Error {
27
33
  function decisionError(decision) {
28
34
  return new PreferenceLearningError('policy-denied', `preference access denied: ${decision.reasonCode}`);
29
35
  }
36
+ /** Fixed Policy subject used by the non-model supervised-growth runbook. */
37
+ export const HOST_RECOVERY_BACKGROUND_ID = 'dsh-enhanced-assistant-recovery';
38
+ const preferenceHostScopeBrand = Symbol('preference-learning.host-scope');
39
+ export function canonicalPreferenceHostScope(input) {
40
+ try {
41
+ const { scope } = canonicalPreferenceScope(input);
42
+ const branded = { workspace: scope.workspace, preset: scope.preset };
43
+ Object.defineProperty(branded, preferenceHostScopeBrand, {
44
+ value: true, enumerable: false, configurable: false, writable: false,
45
+ });
46
+ return Object.freeze(branded);
47
+ }
48
+ catch {
49
+ throw new PreferenceLearningError('invalid-scope', 'Host preference scope is invalid');
50
+ }
51
+ }
52
+ function hostText(value, label, maxBytes, code = 'invalid-input') {
53
+ if (typeof value !== 'string') {
54
+ throw new PreferenceLearningError(code, `${label} must be a string`);
55
+ }
56
+ const normalized = value.normalize('NFC').trim();
57
+ if (normalized === '' || Buffer.byteLength(normalized, 'utf8') > maxBytes || hasControl(normalized)) {
58
+ throw new PreferenceLearningError(code, `${label} is invalid`);
59
+ }
60
+ return normalized;
61
+ }
62
+ function hasControl(value) {
63
+ for (const character of value) {
64
+ const codePoint = character.codePointAt(0);
65
+ if (codePoint <= 0x1f || codePoint === 0x7f)
66
+ return true;
67
+ }
68
+ return false;
69
+ }
70
+ function exactHostScope(input) {
71
+ if (typeof input !== 'object' || input === null || !Object.isFrozen(input)
72
+ || input[preferenceHostScopeBrand] !== true) {
73
+ throw new PreferenceLearningError('invalid-scope', 'Host preference operations require a canonical immutable scope');
74
+ }
75
+ const canonical = canonicalPreferenceHostScope(input);
76
+ if (canonical.workspace !== input.workspace || canonical.preset !== input.preset) {
77
+ throw new PreferenceLearningError('invalid-scope', 'Host preference scope is not canonical');
78
+ }
79
+ return input;
80
+ }
81
+ function isDeliveryPreferenceProducer(value) {
82
+ return isTrustedDeliveryPreferenceProducer(value)
83
+ && typeof value
84
+ .trustedPreferenceProducerGeneration === 'function'
85
+ && typeof value.registerTrustedPreferenceSink === 'function'
86
+ && typeof value.currentPreferenceTurn === 'function'
87
+ && typeof value.preferencePrincipalForAgent === 'function';
88
+ }
89
+ function registrationOwnedByPersonalMemory(registration) {
90
+ try {
91
+ return typeof registration.owner === 'object' && registration.owner !== null
92
+ && typeof registration.owner.ownsPreferencePromotionSourceRegistration === 'function'
93
+ && registration.owner.ownsPreferencePromotionSourceRegistration(registration);
94
+ }
95
+ catch {
96
+ return false;
97
+ }
98
+ }
30
99
  export class PreferenceLearningService extends Service {
31
100
  static Config = configSchema;
32
101
  static inject = ['assistantPolicy', 'systemPrompt'];
33
102
  store;
34
103
  policy;
35
104
  config;
105
+ activeDeliveryRegistrations = new WeakSet();
106
+ memoryPromotionProducerGeneration = `preference-${randomUUID()}`;
107
+ /** Re-entrancy guard only; durable ownership and retry state remain in SQLite. */
108
+ memoryPromotionReconcileRunning = false;
109
+ memoryPromotionReconcileScheduled = false;
110
+ memoryPromotionBinding;
111
+ deliveryBinding;
36
112
  active = true;
37
113
  constructor(ctx, input, options = {}) {
38
114
  super(ctx, 'assistantPreferenceLearning');
115
+ brandPreferenceMemoryPromotionProducer(this);
39
116
  try {
40
117
  this.config = configSchema(input);
41
118
  }
@@ -55,11 +132,16 @@ export class PreferenceLearningService extends Service {
55
132
  signalTtlMs: this.config.signalTtlMs,
56
133
  hypothesisTtlMs: this.config.hypothesisTtlMs,
57
134
  minSignalsForActivation: this.config.minSignalsForActivation,
135
+ minBehavioralSignalsForActivation: this.config.minBehavioralSignalsForActivation,
58
136
  minConfidenceBps: this.config.minConfidenceBps,
59
137
  maxContradictionBps: this.config.maxContradictionBps,
60
138
  maxActiveOverlays: this.config.maxActiveOverlays,
61
139
  maxReviewHypotheses: this.config.maxReviewHypotheses,
62
140
  maxOverlayBytes: this.config.maxOverlayBytes,
141
+ minSignalsForPromotion: 3,
142
+ minConfidenceBpsForPromotion: 8_500,
143
+ maxContradictionBpsForPromotion: 1_500,
144
+ promotionProposalTtlMs: Math.min(this.config.hypothesisTtlMs, 604_800_000),
63
145
  });
64
146
  // Runtime context is re-evaluated before every model step. The AgentLoop
65
147
  // projection replaces or clears its prior snapshot, so rollback, expiry,
@@ -69,13 +151,15 @@ export class PreferenceLearningService extends Service {
69
151
  order: 250,
70
152
  text: ({ agent }) => this.dynamicOverlay(agent),
71
153
  });
154
+ const currentDelivery = ctx.get('assistantDelivery');
155
+ if (isDeliveryPreferenceProducer(currentDelivery)) {
156
+ this.bindDeliveryProducer(currentDelivery);
157
+ }
72
158
  ctx.inject(['assistantDelivery'], deliveryCtx => {
73
- if (!this.config.enabled)
74
- return;
75
159
  const delivery = deliveryCtx.get('assistantDelivery');
76
- if (delivery === undefined || typeof delivery.subscribePreferenceFeedback !== 'function')
160
+ if (!isDeliveryPreferenceProducer(delivery))
77
161
  return;
78
- return delivery.subscribePreferenceFeedback(events => this.appendDeliveryFeedbackBatch(events));
162
+ return this.bindDeliveryProducer(delivery);
79
163
  });
80
164
  const registeredTools = ctx.get('tools');
81
165
  const disposeCurrentTools = registeredTools === undefined
@@ -88,11 +172,19 @@ export class PreferenceLearningService extends Service {
88
172
  return registerPreferenceTools(tools, this);
89
173
  });
90
174
  this.store.maintain(this.config.maintenanceBatchSize);
175
+ if (this.config.enabled && this.config.autonomousT1Enabled) {
176
+ this.store.activateReadyScopes(this.config.maintenanceBatchSize);
177
+ }
91
178
  const maintenanceTimer = setInterval(() => {
92
179
  if (!this.active)
93
180
  return;
94
181
  try {
95
182
  this.store.maintain(this.config.maintenanceBatchSize);
183
+ if (this.config.enabled && this.config.autonomousT1Enabled) {
184
+ this.store.activateReadyScopes(this.config.maintenanceBatchSize);
185
+ }
186
+ if (this.memoryPromotionBinding !== undefined)
187
+ this.reconcileMemoryPromotions();
96
188
  }
97
189
  catch {
98
190
  ctx.logger.warn('preference-learning: bounded retention maintenance failed');
@@ -101,11 +193,150 @@ export class PreferenceLearningService extends Service {
101
193
  maintenanceTimer.unref();
102
194
  ctx.effect(() => () => {
103
195
  this.active = false;
196
+ unbrandPreferenceMemoryPromotionProducer(this);
104
197
  clearInterval(maintenanceTimer);
198
+ this.deliveryBinding?.dispose();
199
+ this.deliveryBinding = undefined;
200
+ this.memoryPromotionBinding?.dispose();
201
+ this.memoryPromotionBinding = undefined;
105
202
  disposeCurrentTools?.();
106
203
  this.store.close();
107
204
  }, 'preference-learning.database');
108
205
  }
206
+ trustedMemoryPromotionProducerGeneration() {
207
+ this.assertActive();
208
+ return this.memoryPromotionProducerGeneration;
209
+ }
210
+ /** Accept one exact Memory-owned, process-local bidirectional registration. */
211
+ registerTrustedMemoryPromotionResultSink(registration) {
212
+ this.assertActive();
213
+ if (registration.protocol !== PREFERENCE_MEMORY_PROMOTION_PROTOCOL
214
+ || registration.producer !== 'personal-memory'
215
+ || registration.sourceGeneration !== this.memoryPromotionProducerGeneration
216
+ || typeof registration.sinkGeneration !== 'string'
217
+ || registration.sinkGeneration.normalize('NFC').trim() !== registration.sinkGeneration
218
+ || registration.sinkGeneration === ''
219
+ || Buffer.byteLength(registration.sinkGeneration, 'utf8') > 200
220
+ || typeof registration.propose !== 'function'
221
+ || typeof registration.cancelPromotion !== 'function'
222
+ || typeof registration.listTerminalResults !== 'function'
223
+ || typeof registration.acknowledgeTerminalResult !== 'function'
224
+ || !registrationOwnedByPersonalMemory(registration)) {
225
+ throw new PreferenceLearningError('unattested-signal', 'trusted Memory promotion registration is invalid');
226
+ }
227
+ const current = this.memoryPromotionBinding;
228
+ if (current !== undefined) {
229
+ if (current.registration === registration)
230
+ return current.dispose;
231
+ throw new PreferenceLearningError('unattested-signal', 'trusted Memory promotion sink is already registered');
232
+ }
233
+ const token = Symbol('preference-learning.memory-promotion');
234
+ let live = true;
235
+ const dispose = () => {
236
+ if (!live)
237
+ return;
238
+ live = false;
239
+ if (this.memoryPromotionBinding?.token === token)
240
+ this.memoryPromotionBinding = undefined;
241
+ };
242
+ this.memoryPromotionBinding = Object.freeze({ token, registration, dispose });
243
+ this.reconcileMemoryPromotions();
244
+ return dispose;
245
+ }
246
+ /**
247
+ * Bounded durable producer drain. Failures stay in retry_wait; one malformed
248
+ * or unavailable Memory row never drops the remaining outbox.
249
+ */
250
+ reconcileMemoryPromotions(limit = this.config.promotionReconcileLimit) {
251
+ this.assertActive();
252
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1_000) {
253
+ throw new PreferenceLearningError('invalid-input', 'promotion reconcile limit is invalid');
254
+ }
255
+ const binding = this.memoryPromotionBinding;
256
+ if (binding === undefined)
257
+ return Object.freeze({ submitted: 0, cancelled: 0, projected: 0 });
258
+ if (this.memoryPromotionReconcileRunning) {
259
+ return Object.freeze({ submitted: 0, cancelled: 0, projected: 0 });
260
+ }
261
+ this.memoryPromotionReconcileRunning = true;
262
+ let submitted = 0;
263
+ let cancelled = 0;
264
+ let projected = 0;
265
+ const current = () => this.memoryPromotionBinding?.token === binding.token
266
+ && binding.registration.sourceGeneration === this.memoryPromotionProducerGeneration
267
+ && registrationOwnedByPersonalMemory(binding.registration);
268
+ try {
269
+ for (const request of this.store.listPendingMemoryPromotionCancellations(limit)) {
270
+ if (!current())
271
+ return Object.freeze({ submitted, cancelled, projected });
272
+ try {
273
+ const receipt = validatePreferenceMemoryPromotionCancellationReceipt(binding.registration.cancelPromotion(request), request);
274
+ this.store.settleMemoryPromotionCancellation({ request, receipt });
275
+ cancelled += 1;
276
+ }
277
+ catch (error) {
278
+ this.store.settleMemoryPromotionCancellation({
279
+ request,
280
+ retryError: this.promotionFailureCode(error),
281
+ });
282
+ }
283
+ }
284
+ for (const entry of this.store.listPendingMemoryPromotions(limit)) {
285
+ if (!current())
286
+ return Object.freeze({ submitted, cancelled, projected });
287
+ try {
288
+ const receipt = validatePreferenceMemoryPromotionSubmissionReceipt(binding.registration.propose(entry.request), entry.request);
289
+ this.store.settleMemoryPromotionSubmission({ request: entry.request, receipt });
290
+ submitted += 1;
291
+ }
292
+ catch (error) {
293
+ this.store.settleMemoryPromotionSubmission({
294
+ request: entry.request,
295
+ retryError: this.promotionFailureCode(error),
296
+ });
297
+ }
298
+ }
299
+ if (current()) {
300
+ const results = binding.registration.listTerminalResults(limit);
301
+ if (!Array.isArray(results) || results.length > limit) {
302
+ throw new PreferenceLearningError('unattested-signal', 'Memory returned an invalid result batch');
303
+ }
304
+ for (const raw of results) {
305
+ if (!current())
306
+ break;
307
+ const result = validatePreferenceMemoryPromotionResult(raw);
308
+ const acknowledgement = this.store.projectMemoryPromotionResult(result);
309
+ binding.registration.acknowledgeTerminalResult(acknowledgement);
310
+ projected += 1;
311
+ }
312
+ }
313
+ return Object.freeze({ submitted, cancelled, projected });
314
+ }
315
+ finally {
316
+ this.memoryPromotionReconcileRunning = false;
317
+ }
318
+ }
319
+ promotionFailureCode(error) {
320
+ const name = error instanceof Error ? error.name : 'unknown-error';
321
+ return `memory-${name.normalize('NFC').toLowerCase().replaceAll(/[^a-z0-9]+/gu, '-').slice(0, 80)}`;
322
+ }
323
+ /** Never call Memory while Delivery is still holding its preference writer lock. */
324
+ scheduleMemoryPromotionReconcile() {
325
+ if (this.memoryPromotionBinding === undefined || this.memoryPromotionReconcileScheduled)
326
+ return;
327
+ this.memoryPromotionReconcileScheduled = true;
328
+ queueMicrotask(() => {
329
+ this.memoryPromotionReconcileScheduled = false;
330
+ if (!this.active || this.memoryPromotionBinding === undefined)
331
+ return;
332
+ try {
333
+ this.reconcileMemoryPromotions();
334
+ }
335
+ catch {
336
+ this.ctx.logger.warn('preference-learning: Memory promotion reconciliation failed');
337
+ }
338
+ });
339
+ }
109
340
  /**
110
341
  * Unprivileged observation seam. Actor/source authority is assigned here;
111
342
  * callers cannot use it to claim owner feedback or unlock an effect.
@@ -128,23 +359,101 @@ export class PreferenceLearningService extends Service {
128
359
  }
129
360
  review(agent, limit) {
130
361
  this.assertActive();
131
- const scope = this.authorizedScope(agent, 'review', 'hypotheses');
132
- const hypotheses = this.store.list(scope, limit);
362
+ const { scope, principalId, principalLineage } = this.authorizedPrincipalScope(agent, 'review', 'hypotheses');
363
+ const hypotheses = this.store.list(scope, limit, principalId, principalLineage);
133
364
  return Object.freeze({
134
365
  hypotheses: Object.freeze(hypotheses),
135
- activeOverlay: this.config.enabled ? this.store.overlay(scope) : undefined,
366
+ activeOverlay: this.config.enabled
367
+ ? this.store.overlaySnapshot(scope, principalId, principalLineage).text
368
+ : undefined,
369
+ });
370
+ }
371
+ /** Host-only, Agent-free review for the deterministic recovery runbook. */
372
+ hostReview(input) {
373
+ this.assertActive();
374
+ const scope = this.authorizedHostScope(input, 'inspect', 'hypotheses');
375
+ const principalId = hostText(input.principal, 'principal', 500, 'missing-principal');
376
+ const fence = this.hostPrincipalFence(input);
377
+ const hypotheses = this.store.list(scope, input.limit, principalId, {
378
+ principalRecordId: fence.principalLineageId,
379
+ principalVersion: fence.principalLineageVersion,
380
+ });
381
+ return Object.freeze({
382
+ hypotheses: Object.freeze(hypotheses),
383
+ activeOverlay: this.config.enabled
384
+ ? this.store.overlaySnapshot(scope, principalId, {
385
+ principalRecordId: fence.principalLineageId,
386
+ principalVersion: fence.principalLineageVersion,
387
+ }).text
388
+ : undefined,
389
+ });
390
+ }
391
+ /** Side-effect-free Preference-state fence capture for a fixed Host plan. */
392
+ hostOwnerFence(input) {
393
+ this.assertActive();
394
+ this.authorizedHostScope(input, 'inspect', 'owner-fence');
395
+ const fence = this.hostPrincipalFence(input);
396
+ return Object.freeze({
397
+ ownerGeneration: fence.generation,
398
+ principalLineage: Object.freeze({
399
+ principalRecordId: fence.principalLineageId,
400
+ principalVersion: fence.principalLineageVersion,
401
+ }),
402
+ });
403
+ }
404
+ /**
405
+ * Return one deterministic, content-minimal activation target. The Store
406
+ * evaluates the exact same readiness predicates used by activate().
407
+ */
408
+ hostActivationCandidate(input) {
409
+ this.assertActive();
410
+ this.assertEnabled();
411
+ const scope = this.authorizedHostScope(input, 'inspect', 'activation-candidate');
412
+ const fence = this.hostPrincipalFence(input);
413
+ const candidate = this.store.activationCandidate(scope, fence);
414
+ if (candidate === undefined)
415
+ return undefined;
416
+ return Object.freeze({
417
+ hypothesisId: candidate.id,
418
+ expectedVersion: candidate.version,
419
+ ownerGeneration: fence.generation,
420
+ principalLineage: Object.freeze({
421
+ principalRecordId: fence.principalLineageId,
422
+ principalVersion: fence.principalLineageVersion,
423
+ }),
136
424
  });
137
425
  }
138
426
  activate(agent, hypothesisId, expectedVersion) {
139
427
  this.assertActive();
140
428
  this.assertEnabled();
141
- const scope = this.authorizedScope(agent, 'activate', hypothesisId);
142
- return this.store.activate(scope, hypothesisId, expectedVersion);
429
+ const principal = this.authorizedPrincipalScope(agent, 'activate', hypothesisId);
430
+ return this.store.activate(principal.scope, hypothesisId, expectedVersion, this.requirePrincipalFence(principal));
431
+ }
432
+ /** Activate exactly one evidence-ready, owner-attested T1 hypothesis by CAS. */
433
+ hostActivateOne(input) {
434
+ this.assertActive();
435
+ const hypothesisId = hostText(input.hypothesisId, 'hypothesisId', 200);
436
+ const scope = this.authorizedHostScope(input, 'activate', `hypothesis:${hypothesisId}`);
437
+ const fence = this.hostPrincipalFence(input);
438
+ return this.store.activateHostOnce(scope, hypothesisId, input.expectedVersion, input.operationId, fence, this.config.enabled);
143
439
  }
144
440
  rollback(agent, hypothesisId, expectedVersion, reason) {
145
441
  this.assertActive();
146
- const scope = this.authorizedScope(agent, 'rollback', hypothesisId);
147
- return this.store.rollback(scope, hypothesisId, expectedVersion, reason);
442
+ const principal = this.authorizedPrincipalScope(agent, 'rollback', hypothesisId);
443
+ return this.store.rollback(principal.scope, hypothesisId, expectedVersion, reason, this.requirePrincipalFence(principal));
444
+ }
445
+ /** Roll back exactly one currently-active T1 overlay by CAS. */
446
+ hostRollbackOne(input) {
447
+ this.assertActive();
448
+ const hypothesisId = hostText(input.hypothesisId, 'hypothesisId', 200);
449
+ const scope = this.authorizedHostScope(input, 'rollback', `hypothesis:${hypothesisId}`);
450
+ const principalId = hostText(input.principal, 'principal', 500, 'missing-principal');
451
+ const fence = this.hostPrincipalFence(input);
452
+ const current = this.store.get(scope, hypothesisId, principalId, input.principalLineage);
453
+ if (current?.riskTier !== 'T1' || current.effectState !== 'active') {
454
+ throw new PreferenceLearningError('unattested-signal', 'Host recovery can only roll back one exact active T1 preference');
455
+ }
456
+ return this.store.rollback(scope, hypothesisId, input.expectedVersion, 'regression', fence);
148
457
  }
149
458
  /** Host-only privacy seam. It removes exact-scope rows and retains only a scope digest tombstone. */
150
459
  forgetScope(scope, idempotencyKey) {
@@ -161,6 +470,7 @@ export class PreferenceLearningService extends Service {
161
470
  signalTtlMs: this.config.signalTtlMs,
162
471
  hypothesisTtlMs: this.config.hypothesisTtlMs,
163
472
  minSignalsForActivation: this.config.minSignalsForActivation,
473
+ minBehavioralSignalsForActivation: this.config.minBehavioralSignalsForActivation,
164
474
  minConfidenceBps: this.config.minConfidenceBps,
165
475
  maxContradictionBps: this.config.maxContradictionBps,
166
476
  maxActiveOverlays: this.config.maxActiveOverlays,
@@ -175,40 +485,411 @@ export class PreferenceLearningService extends Service {
175
485
  this.assertActive();
176
486
  return this.store.maintain(this.config.maintenanceBatchSize);
177
487
  }
488
+ /** Policy-authorized, exact-scope retention step; deletes at most one row. */
489
+ hostMaintainOne(input) {
490
+ this.assertActive();
491
+ const scope = this.authorizedHostScope(input, 'maintain', 'retention');
492
+ return this.store.maintainScopeOnce(scope, input.operationId, this.hostPrincipalFence(input));
493
+ }
178
494
  /** Host integration seam for foreground prompt composition. */
179
495
  overlayForAgent(agent) {
180
496
  this.assertActive();
181
497
  if (!this.config.enabled)
182
498
  return undefined;
183
- const scope = this.authorizedScope(agent, 'snapshot', 'active');
184
- return this.store.overlay(scope);
499
+ const { scope, principalId, principalLineage } = this.authorizedPrincipalScope(agent, 'snapshot', 'active');
500
+ return this.store.overlaySnapshot(scope, principalId, principalLineage).text;
501
+ }
502
+ /** Exact process-local ownership proof; copied or disposed registrations never pass. */
503
+ ownsDeliveryPreferenceRegistration(registration) {
504
+ return this.active && typeof registration === 'object' && registration !== null
505
+ && this.activeDeliveryRegistrations.has(registration);
506
+ }
507
+ bindDeliveryProducer(producer) {
508
+ let generation;
509
+ try {
510
+ generation = hostText(producer.trustedPreferenceProducerGeneration(), 'Delivery preference producer generation', 200);
511
+ }
512
+ catch {
513
+ throw new PreferenceLearningError('unattested-signal', 'Delivery preference producer is invalid');
514
+ }
515
+ if (this.deliveryBinding?.generation === generation)
516
+ return this.deliveryBinding.dispose;
517
+ let live = true;
518
+ let registration;
519
+ registration = Object.freeze({
520
+ protocol: DELIVERY_PREFERENCE_PROJECTION_PROTOCOL,
521
+ producer: 'preference-learning',
522
+ generation,
523
+ owner: this,
524
+ append: (events) => {
525
+ if (!live || !this.active || !this.activeDeliveryRegistrations.has(registration)
526
+ || producer.trustedPreferenceProducerGeneration() !== generation) {
527
+ throw new PreferenceLearningError('unattested-signal', 'stale Delivery preference registration');
528
+ }
529
+ return this.appendDeliveryPreferenceBatch(events);
530
+ },
531
+ appendSynchronously: (events) => {
532
+ if (!live || !this.active || !this.activeDeliveryRegistrations.has(registration)
533
+ || producer.trustedPreferenceProducerGeneration() !== generation) {
534
+ throw new PreferenceLearningError('unattested-signal', 'stale Delivery preference registration');
535
+ }
536
+ return this.appendDeliveryPreferenceBatch(events);
537
+ },
538
+ control: (request) => {
539
+ if (!live || !this.active || !this.activeDeliveryRegistrations.has(registration)
540
+ || producer.trustedPreferenceProducerGeneration() !== generation) {
541
+ throw new PreferenceLearningError('unattested-signal', 'stale Delivery preference registration');
542
+ }
543
+ return this.controlDeliveryLearning(request);
544
+ },
545
+ });
546
+ this.activeDeliveryRegistrations.add(registration);
547
+ let unregister;
548
+ try {
549
+ unregister = producer.registerTrustedPreferenceSink(registration);
550
+ if (typeof unregister !== 'function') {
551
+ throw new PreferenceLearningError('unattested-signal', 'Delivery preference producer returned no registration disposer');
552
+ }
553
+ }
554
+ catch (error) {
555
+ live = false;
556
+ this.activeDeliveryRegistrations.delete(registration);
557
+ throw error;
558
+ }
559
+ let binding;
560
+ const dispose = () => {
561
+ if (!live)
562
+ return;
563
+ live = false;
564
+ this.activeDeliveryRegistrations.delete(registration);
565
+ if (this.deliveryBinding === binding)
566
+ this.deliveryBinding = undefined;
567
+ unregister?.();
568
+ };
569
+ binding = Object.freeze({ generation, producer, registration, dispose });
570
+ const previous = this.deliveryBinding;
571
+ this.deliveryBinding = binding;
572
+ previous?.dispose();
573
+ return dispose;
185
574
  }
186
575
  dynamicOverlay(agent) {
187
576
  if (!this.config.enabled || agent === undefined)
188
577
  return '';
189
578
  try {
190
- return this.overlayForAgent(agent) ?? '';
579
+ const scope = this.authorizedScope(agent, 'snapshot', 'active');
580
+ const turn = this.deliveryBinding?.producer.currentPreferenceTurn(agent);
581
+ if (turn === undefined
582
+ || turn.scope.workspace !== scope.workspace
583
+ || turn.scope.preset !== scope.preset
584
+ || turn.sessionId !== String(agent.session.id))
585
+ return '';
586
+ const snapshot = this.store.overlaySnapshot(scope, turn.principalId, turn.principalLineage);
587
+ if (snapshot.ownerFence !== undefined) {
588
+ for (const hypothesis of snapshot.hypotheses) {
589
+ this.store.recordExposure({
590
+ scope,
591
+ ownerFence: snapshot.ownerFence,
592
+ hypothesisId: hypothesis.id,
593
+ hypothesisVersion: hypothesis.version,
594
+ sessionId: turn.sessionId,
595
+ sourceEventId: turn.sourceEventId,
596
+ });
597
+ }
598
+ }
599
+ return snapshot.text ?? '';
191
600
  }
192
601
  catch {
193
602
  this.ctx.logger.warn('preference-learning: current scoped overlay is unavailable');
194
603
  return '';
195
604
  }
196
605
  }
606
+ controlDeliveryLearning(request) {
607
+ this.assertActive();
608
+ const principalId = hostText(request.principalId, 'Delivery learning principalId', 500);
609
+ const idempotencyKey = hostText(request.idempotencyKey, 'Delivery learning idempotencyKey', 500);
610
+ if (!Number.isSafeInteger(request.occurredAt) || request.occurredAt < 0
611
+ || !['explain', 'export', 'forget', 'pause', 'resume', 'rollback', 'status'].includes(request.action)
612
+ || (request.action === 'rollback' ? typeof request.preferenceKey !== 'string'
613
+ : request.preferenceKey !== undefined)) {
614
+ throw new PreferenceLearningError('unattested-signal', 'Delivery learning control is invalid');
615
+ }
616
+ const admissionCursor = this.deliveryAdmissionCursor(request.admissionCursor);
617
+ if (admissionCursor === undefined) {
618
+ return Object.freeze({ outcome: 'stale', action: request.action, idempotencyKey });
619
+ }
620
+ const lineage = this.deliveryLineage(request.principalLineage);
621
+ const principal = this.store.ensureScopePrincipal(request.scope, principalId, request.occurredAt, lineage, admissionCursor);
622
+ if (!principal.accepted) {
623
+ return Object.freeze({
624
+ outcome: 'stale',
625
+ action: request.action,
626
+ idempotencyKey,
627
+ });
628
+ }
629
+ try {
630
+ let replayed = false;
631
+ let deletedSignals;
632
+ let deletedHypotheses;
633
+ let explanation;
634
+ let exportDocument;
635
+ let rolledBack;
636
+ let rolledBackVersion;
637
+ let state;
638
+ if (request.action === 'status') {
639
+ const result = this.store.recordScopeLearningStatus(request.scope, principal, admissionCursor, request.occurredAt, idempotencyKey);
640
+ if (!result.applied) {
641
+ return Object.freeze({ outcome: 'stale', action: request.action, idempotencyKey });
642
+ }
643
+ replayed = result.replayed;
644
+ state = result.state;
645
+ }
646
+ else if (request.action === 'explain') {
647
+ const result = this.store.explainScopeLearning(request.scope, principal, admissionCursor, request.occurredAt, idempotencyKey);
648
+ if (!result.applied) {
649
+ return Object.freeze({ outcome: 'stale', action: request.action, idempotencyKey });
650
+ }
651
+ replayed = result.replayed;
652
+ state = result.state;
653
+ explanation = result.explanation;
654
+ }
655
+ else if (request.action === 'export') {
656
+ const result = this.store.exportScopeLearning(request.scope, principal, admissionCursor, request.occurredAt, idempotencyKey);
657
+ if (!result.applied) {
658
+ return Object.freeze({ outcome: 'stale', action: request.action, idempotencyKey });
659
+ }
660
+ replayed = result.replayed;
661
+ state = result.state;
662
+ exportDocument = Object.freeze({
663
+ format: 'dsh-preference-learning',
664
+ version: 1,
665
+ records: result.records,
666
+ });
667
+ }
668
+ else if (request.action === 'pause' || request.action === 'resume') {
669
+ const result = this.store.setScopeLearningPaused(request.scope, principal, request.action === 'pause', admissionCursor, request.occurredAt, idempotencyKey);
670
+ if (!result.applied) {
671
+ return Object.freeze({ outcome: 'stale', action: request.action, idempotencyKey });
672
+ }
673
+ replayed = result.replayed;
674
+ state = result.state;
675
+ }
676
+ else if (request.action === 'rollback') {
677
+ const result = this.store.rollbackScopeLearningKey(request.scope, principal, request.preferenceKey, admissionCursor, request.occurredAt, idempotencyKey);
678
+ if (!result.applied) {
679
+ return Object.freeze({ outcome: 'stale', action: request.action, idempotencyKey });
680
+ }
681
+ replayed = result.replayed;
682
+ state = result.state;
683
+ rolledBack = result.rolledBack;
684
+ rolledBackVersion = result.rolledBackVersion;
685
+ }
686
+ else {
687
+ const forgotten = this.store.forgetScope(request.scope, idempotencyKey, { ownerFence: principal, admissionCursor, occurredAt: request.occurredAt });
688
+ if (!forgotten.applied) {
689
+ return Object.freeze({ outcome: 'stale', action: request.action, idempotencyKey });
690
+ }
691
+ replayed = forgotten.replayed;
692
+ deletedSignals = forgotten.deletedSignals;
693
+ deletedHypotheses = forgotten.deletedHypotheses;
694
+ if (forgotten.state === undefined) {
695
+ throw new PreferenceLearningError('unattested-signal', 'Delivery forget receipt is missing its exact historical state');
696
+ }
697
+ state = forgotten.state;
698
+ }
699
+ this.scheduleMemoryPromotionReconcile();
700
+ return Object.freeze({
701
+ outcome: 'applied',
702
+ action: request.action,
703
+ idempotencyKey,
704
+ replayed,
705
+ state: this.deliveryLearningStatus(state),
706
+ ...(deletedSignals === undefined ? {} : { deletedSignals }),
707
+ ...(deletedHypotheses === undefined ? {} : { deletedHypotheses }),
708
+ ...(explanation === undefined ? {} : { explanation }),
709
+ ...(exportDocument === undefined ? {} : { exportDocument }),
710
+ ...(rolledBack === undefined ? {} : { rolledBack }),
711
+ ...(rolledBackVersion === undefined ? {} : { rolledBackVersion }),
712
+ });
713
+ }
714
+ catch (error) {
715
+ if (error instanceof PreferenceStoreError && error.code === 'conflict') {
716
+ return Object.freeze({
717
+ outcome: 'stale',
718
+ action: request.action,
719
+ idempotencyKey,
720
+ });
721
+ }
722
+ throw error;
723
+ }
724
+ }
725
+ deliveryLearningStatus(state) {
726
+ const administrativelyEnabled = this.config.enabled;
727
+ const effectiveActiveOverlays = administrativelyEnabled && state.mode === 'active'
728
+ ? state.storedActiveOverlays
729
+ : 0;
730
+ return Object.freeze({
731
+ mode: administrativelyEnabled ? state.mode : 'disabled',
732
+ administrativelyEnabled,
733
+ collectionMode: state.mode,
734
+ signals: state.signals,
735
+ hypotheses: state.hypotheses,
736
+ storedActiveOverlays: state.storedActiveOverlays,
737
+ effectiveActiveOverlays,
738
+ activeOverlays: effectiveActiveOverlays,
739
+ shadowHypotheses: state.shadowHypotheses,
740
+ });
741
+ }
742
+ deliveryLineage(input) {
743
+ if (typeof input !== 'object' || input === null
744
+ || typeof input.principalRecordId !== 'string'
745
+ || input.principalRecordId.normalize('NFC').trim() !== input.principalRecordId
746
+ || input.principalRecordId.length < 1
747
+ || Buffer.byteLength(input.principalRecordId) > 500
748
+ || !Number.isSafeInteger(input.principalVersion) || input.principalVersion < 1) {
749
+ throw new PreferenceLearningError('unattested-signal', 'Delivery owner lineage is invalid');
750
+ }
751
+ return Object.freeze({
752
+ principalRecordId: input.principalRecordId,
753
+ principalVersion: input.principalVersion,
754
+ });
755
+ }
756
+ deliveryAdmissionCursor(input) {
757
+ if (input === undefined)
758
+ return undefined;
759
+ if (typeof input !== 'object' || input === null
760
+ || typeof input.epoch !== 'string'
761
+ || !/^[0-9a-f]{32}$/u.test(input.epoch)
762
+ || !Number.isSafeInteger(input.sequence) || input.sequence < 1) {
763
+ throw new PreferenceLearningError('unattested-signal', 'Delivery admission cursor is invalid');
764
+ }
765
+ return Object.freeze({ epoch: input.epoch, sequence: input.sequence });
766
+ }
197
767
  /**
198
768
  * The closure is registered directly with Assistant Delivery. No public
199
769
  * service method accepts owner-authenticated provenance.
200
770
  */
201
- appendDeliveryFeedbackBatch(events) {
771
+ appendDeliveryPreferenceBatch(events) {
202
772
  this.assertActive();
203
- this.assertEnabled();
204
773
  if (!Array.isArray(events) || events.length < 1 || events.length > 16) {
205
774
  throw new PreferenceLearningError('unattested-signal', 'delivery feedback batch is invalid');
206
775
  }
207
- const signals = events.map(event => {
208
- if (event.stance !== 'support'
209
- || event.actorTrust !== 'owner-authenticated'
210
- || event.interpretationTrust !== 'typed-feedback'
211
- || event.source !== 'direct-owner-feedback') {
776
+ if (!this.config.enabled) {
777
+ return Object.freeze(events.map(event => Object.freeze({
778
+ idempotencyKey: hostText(event.idempotencyKey, 'Delivery preference idempotencyKey', 500),
779
+ status: 'recorded',
780
+ })));
781
+ }
782
+ const first = events[0];
783
+ const principalId = hostText(first.principalId, 'Delivery preference principalId', 500);
784
+ // Projection rows written by a pre-lineage/cursor Delivery build are
785
+ // terminally ignored. Replaying them into a new owner generation or across
786
+ // a pause/forget boundary would be unsafe.
787
+ if (first.principalLineage === undefined
788
+ || first.admissionCursor === undefined
789
+ || events.some(event => event.principalLineage === undefined
790
+ || event.admissionCursor === undefined)) {
791
+ return Object.freeze(events.map(event => Object.freeze({
792
+ idempotencyKey: event.idempotencyKey,
793
+ status: 'recorded',
794
+ })));
795
+ }
796
+ const lineage = this.deliveryLineage(first.principalLineage);
797
+ const admissionCursor = this.deliveryAdmissionCursor(first.admissionCursor);
798
+ if (principalId !== first.principalId
799
+ || events.some(event => event.principalId !== principalId
800
+ || event.scope.workspace !== first.scope.workspace
801
+ || event.scope.preset !== first.scope.preset
802
+ || event.principalLineage?.principalRecordId !== lineage.principalRecordId
803
+ || event.principalLineage?.principalVersion !== lineage.principalVersion
804
+ || event.admissionCursor?.epoch !== admissionCursor.epoch
805
+ || event.admissionCursor?.sequence !== admissionCursor.sequence
806
+ || !Number.isSafeInteger(event.occurredAt) || event.occurredAt < 0)) {
807
+ throw new PreferenceLearningError('unattested-signal', 'delivery preference owner fence is invalid');
808
+ }
809
+ const principal = this.store.ensureScopePrincipal(first.scope, principalId, first.occurredAt, lineage, admissionCursor);
810
+ if (!principal.accepted) {
811
+ return Object.freeze(events.map(event => Object.freeze({
812
+ idempotencyKey: event.idempotencyKey,
813
+ status: 'recorded',
814
+ })));
815
+ }
816
+ const admittedEvents = events.filter(event => this.store.scopeAcceptsEvent(event.scope, principal, admissionCursor));
817
+ if (admittedEvents.length === 0) {
818
+ return Object.freeze(events.map(event => Object.freeze({
819
+ idempotencyKey: event.idempotencyKey,
820
+ status: 'recorded',
821
+ })));
822
+ }
823
+ try {
824
+ for (const event of admittedEvents) {
825
+ if (event.source === 'direct-owner-feedback')
826
+ continue;
827
+ const completion = this.deliveryCompletion(event);
828
+ this.store.bindExposure({
829
+ admissionCursor,
830
+ scope: event.scope,
831
+ ownerFence: principal,
832
+ sessionId: completion.sessionId,
833
+ sourceEventId: completion.sourceEventId,
834
+ sourceInboxId: completion.sourceInboxId,
835
+ replyOutboxId: completion.replyOutboxId,
836
+ });
837
+ }
838
+ const signalEvents = admittedEvents.filter((event) => event.source !== 'delivery-completion');
839
+ const signals = signalEvents.map(event => this.deliverySignal(event));
840
+ if (signals.length > 0) {
841
+ this.store.appendSignals(signals, {
842
+ admissionCursor,
843
+ ownerFence: principal,
844
+ exactCorrections: signalEvents.flatMap((event, signalIndex) => event.source === 'direct-owner-feedback' && event.exposureTarget !== undefined
845
+ ? [Object.freeze({
846
+ signalIndex,
847
+ sourceInboxId: event.exposureTarget.sourceInboxId,
848
+ replyOutboxId: event.exposureTarget.sourceOutboxId,
849
+ })]
850
+ : []),
851
+ });
852
+ this.store.enqueueEligibleMemoryPromotions(first.scope, principalId, principal);
853
+ }
854
+ if (this.config.autonomousT1Enabled) {
855
+ const scopes = new Map(admittedEvents.map(event => [
856
+ JSON.stringify([event.scope.workspace, event.scope.preset]),
857
+ { workspace: event.scope.workspace, preset: event.scope.preset },
858
+ ]));
859
+ for (const scope of scopes.values()) {
860
+ this.store.activateReady(scope, this.config.maxActiveOverlays, principal);
861
+ }
862
+ }
863
+ }
864
+ catch (error) {
865
+ if (!(error instanceof PreferenceStoreError && error.code === 'learning-paused'))
866
+ throw error;
867
+ }
868
+ this.scheduleMemoryPromotionReconcile();
869
+ return Object.freeze(events.map(event => Object.freeze({
870
+ idempotencyKey: event.idempotencyKey,
871
+ status: 'recorded',
872
+ })));
873
+ }
874
+ deliveryCompletion(event) {
875
+ const completion = event.completion;
876
+ if (event.actorTrust !== 'owner-authenticated'
877
+ || typeof completion !== 'object' || completion === null
878
+ || !Number.isSafeInteger(completion.bindingVersion) || completion.bindingVersion < 1
879
+ || [completion.bindingId, completion.sessionId, completion.sourceEventId,
880
+ completion.sourceInboxId, completion.replyOutboxId]
881
+ .some(value => typeof value !== 'string' || value.length < 1 || Buffer.byteLength(value) > 500)) {
882
+ throw new PreferenceLearningError('unattested-signal', 'delivery completion attestation is invalid');
883
+ }
884
+ return completion;
885
+ }
886
+ deliverySignal(event) {
887
+ if (event.stance !== 'support' || event.actorTrust !== 'owner-authenticated') {
888
+ throw new PreferenceLearningError('unattested-signal', 'delivery preference attestation is invalid');
889
+ }
890
+ if (event.source === 'direct-owner-feedback') {
891
+ if (event.interpretationTrust !== 'explicit-selection'
892
+ && event.interpretationTrust !== 'typed-feedback') {
212
893
  throw new PreferenceLearningError('unattested-signal', 'delivery feedback attestation is invalid');
213
894
  }
214
895
  return {
@@ -217,17 +898,31 @@ export class PreferenceLearningService extends Service {
217
898
  candidateValue: event.candidateValue,
218
899
  stance: 'support',
219
900
  actorTrust: 'owner-authenticated',
220
- interpretationTrust: 'typed-feedback',
901
+ interpretationTrust: event.interpretationTrust,
221
902
  source: 'direct-owner-feedback',
222
903
  occurredAt: event.occurredAt,
223
904
  idempotencyKey: `assistant-delivery:${event.idempotencyKey}`,
224
905
  };
225
- });
226
- this.store.appendSignals(signals);
227
- return Object.freeze(events.map(event => Object.freeze({
228
- idempotencyKey: event.idempotencyKey,
229
- status: 'recorded',
230
- })));
906
+ }
907
+ const observation = event;
908
+ this.deliveryCompletion(observation);
909
+ if (observation.source !== 'delivery-observation'
910
+ || observation.interpretationTrust !== 'behavioral-inference'
911
+ || observation.preferenceKey !== 'response.language'
912
+ || (observation.candidateValue !== 'zh-CN' && observation.candidateValue !== 'en')) {
913
+ throw new PreferenceLearningError('unattested-signal', 'delivery observation attestation is invalid');
914
+ }
915
+ return {
916
+ scope: { workspace: observation.scope.workspace, preset: observation.scope.preset },
917
+ preferenceKey: 'response.language',
918
+ candidateValue: observation.candidateValue,
919
+ stance: 'support',
920
+ actorTrust: 'owner-authenticated',
921
+ interpretationTrust: 'behavioral-inference',
922
+ source: 'delivery-observation',
923
+ occurredAt: observation.occurredAt,
924
+ idempotencyKey: `assistant-delivery:${observation.idempotencyKey}`,
925
+ };
231
926
  }
232
927
  authorizedScope(agent, action, resourceId) {
233
928
  const header = agent?.session?.header;
@@ -247,6 +942,76 @@ export class PreferenceLearningService extends Service {
247
942
  throw decisionError(decision);
248
943
  return scope;
249
944
  }
945
+ authorizedPrincipalScope(agent, action, resourceId) {
946
+ const scope = this.authorizedScope(agent, action, resourceId);
947
+ const principal = agent === undefined
948
+ ? undefined
949
+ : this.deliveryBinding?.producer.preferencePrincipalForAgent(agent);
950
+ if (principal === undefined
951
+ || principal.scope.workspace !== scope.workspace
952
+ || principal.scope.preset !== scope.preset
953
+ || principal.sessionId !== String(agent.session.id)) {
954
+ throw new PreferenceLearningError('missing-agent', `missing-agent: preference ${action} requires an exact owner Delivery binding`);
955
+ }
956
+ return Object.freeze({
957
+ scope,
958
+ principalId: hostText(principal.principalId, 'Delivery preference principalId', 500),
959
+ principalLineage: this.deliveryLineage(principal.principalLineage),
960
+ });
961
+ }
962
+ requirePrincipalFence(input) {
963
+ const fence = this.store.scopePrincipalFence(input.scope, input.principalId, input.principalLineage);
964
+ if (fence === undefined) {
965
+ throw new PreferenceLearningError('missing-principal', 'Delivery owner lineage is not current for this preference scope');
966
+ }
967
+ return fence;
968
+ }
969
+ authorizedHostScope(input, action, resourceId) {
970
+ const scope = exactHostScope(input.scope);
971
+ const principal = hostText(input.principal, 'principal', 500, 'missing-principal');
972
+ const principalLineage = this.deliveryLineage(input.principalLineage);
973
+ if (input.ownerGeneration !== undefined
974
+ && (!Number.isSafeInteger(input.ownerGeneration) || input.ownerGeneration < 1)) {
975
+ throw new PreferenceLearningError('invalid-input', 'ownerGeneration is invalid');
976
+ }
977
+ const operationId = hostText(input.operationId, 'operationId', 500);
978
+ const idempotencyDigest = createHash('sha256').update(JSON.stringify([
979
+ HOST_RECOVERY_BACKGROUND_ID,
980
+ scope.workspace,
981
+ scope.preset,
982
+ principal,
983
+ principalLineage.principalRecordId,
984
+ principalLineage.principalVersion,
985
+ input.ownerGeneration ?? 'current',
986
+ action,
987
+ resourceId,
988
+ operationId,
989
+ ])).digest('hex');
990
+ const decision = this.policy.authorize({
991
+ subject: {
992
+ kind: 'background',
993
+ id: HOST_RECOVERY_BACKGROUND_ID,
994
+ workspace: scope.workspace,
995
+ principal,
996
+ },
997
+ action,
998
+ resource: { kind: 'preference', id: resourceId },
999
+ context: { initiator: 'background' },
1000
+ }, { idempotencyKey: `preference-host:${idempotencyDigest}` });
1001
+ if (decision.effect !== 'allow')
1002
+ throw decisionError(decision);
1003
+ return scope;
1004
+ }
1005
+ hostPrincipalFence(input) {
1006
+ const scope = exactHostScope(input.scope);
1007
+ const principal = hostText(input.principal, 'principal', 500, 'missing-principal');
1008
+ const lineage = this.deliveryLineage(input.principalLineage);
1009
+ const fence = this.store.scopePrincipalFence(scope, principal, lineage, input.ownerGeneration);
1010
+ if (fence === undefined) {
1011
+ throw new PreferenceLearningError('missing-principal', 'Host preference owner lineage is stale or unavailable');
1012
+ }
1013
+ return fence;
1014
+ }
250
1015
  assertEnabled() {
251
1016
  if (!this.config.enabled)
252
1017
  throw new PreferenceLearningError('disabled', 'preference learning is disabled');