@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/README.md +50 -11
- package/cordis.patch.yml +3 -0
- package/lib/catalog.d.ts.map +1 -1
- package/lib/catalog.js +7 -6
- package/lib/catalog.js.map +1 -1
- package/lib/index.d.ts +4 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +3 -2
- package/lib/index.js.map +1 -1
- package/lib/service.d.ts +104 -3
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +794 -29
- package/lib/service.js.map +1 -1
- package/lib/sqlite.d.ts +11 -1
- package/lib/sqlite.d.ts.map +1 -1
- package/lib/sqlite.js +643 -2
- package/lib/sqlite.js.map +1 -1
- package/lib/store.d.ts +233 -8
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +2581 -85
- package/lib/store.js.map +1 -1
- package/lib/types.d.ts +1 -0
- package/lib/types.d.ts.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.d.ts.map +1 -1
- package/lib/version.js +1 -1
- package/lib/version.js.map +1 -1
- package/package.json +7 -5
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 {
|
|
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
|
|
160
|
+
if (!isDeliveryPreferenceProducer(delivery))
|
|
77
161
|
return;
|
|
78
|
-
return
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
184
|
-
return this.store.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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:
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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');
|