@dsh-enhanced/assistant-evaluation 0.1.65 → 0.1.67
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 +69 -1
- package/lib/attention-owner-questions.d.ts +104 -0
- package/lib/attention-owner-questions.d.ts.map +1 -0
- package/lib/attention-owner-questions.js +179 -0
- package/lib/attention-owner-questions.js.map +1 -0
- package/lib/attention-tuning.d.ts +492 -0
- package/lib/attention-tuning.d.ts.map +1 -0
- package/lib/attention-tuning.js +1044 -0
- package/lib/attention-tuning.js.map +1 -0
- package/lib/benchmark/cli.d.ts.map +1 -1
- package/lib/benchmark/cli.js +14 -11
- package/lib/benchmark/cli.js.map +1 -1
- package/lib/benchmark/corpus.d.ts +13 -2
- package/lib/benchmark/corpus.d.ts.map +1 -1
- package/lib/benchmark/corpus.js +19 -9
- package/lib/benchmark/corpus.js.map +1 -1
- package/lib/benchmark/families-corpus.d.ts +11 -0
- package/lib/benchmark/families-corpus.d.ts.map +1 -0
- package/lib/benchmark/families-corpus.js +133 -0
- package/lib/benchmark/families-corpus.js.map +1 -0
- package/lib/benchmark/holdout-isolation.d.ts +27 -0
- package/lib/benchmark/holdout-isolation.d.ts.map +1 -0
- package/lib/benchmark/holdout-isolation.js +66 -0
- package/lib/benchmark/holdout-isolation.js.map +1 -0
- package/lib/benchmark/holdout.d.ts.map +1 -1
- package/lib/benchmark/holdout.js +3 -0
- package/lib/benchmark/holdout.js.map +1 -1
- package/lib/benchmark/index.d.ts +3 -0
- package/lib/benchmark/index.d.ts.map +1 -1
- package/lib/benchmark/index.js +2 -0
- package/lib/benchmark/index.js.map +1 -1
- package/lib/benchmark/native-skills.d.ts +12 -14
- package/lib/benchmark/native-skills.d.ts.map +1 -1
- package/lib/benchmark/native-skills.js +1 -3
- package/lib/benchmark/native-skills.js.map +1 -1
- package/lib/benchmark/native.d.ts +15 -2
- package/lib/benchmark/native.d.ts.map +1 -1
- package/lib/benchmark/native.js +67 -14
- package/lib/benchmark/native.js.map +1 -1
- package/lib/benchmark/report.d.ts.map +1 -1
- package/lib/benchmark/report.js +98 -8
- package/lib/benchmark/report.js.map +1 -1
- package/lib/benchmark/runner.d.ts.map +1 -1
- package/lib/benchmark/runner.js +5 -0
- package/lib/benchmark/runner.js.map +1 -1
- package/lib/benchmark/strategy-capabilities.d.ts.map +1 -1
- package/lib/benchmark/strategy-capabilities.js +0 -5
- package/lib/benchmark/strategy-capabilities.js.map +1 -1
- package/lib/benchmark/types.d.ts +64 -7
- package/lib/benchmark/types.d.ts.map +1 -1
- package/lib/index.d.ts +8 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +4 -0
- package/lib/index.js.map +1 -1
- package/lib/intake-attention-agreement.d.ts +63 -0
- package/lib/intake-attention-agreement.d.ts.map +1 -0
- package/lib/intake-attention-agreement.js +146 -0
- package/lib/intake-attention-agreement.js.map +1 -0
- package/lib/notification-outcomes.d.ts +56 -0
- package/lib/notification-outcomes.d.ts.map +1 -0
- package/lib/notification-outcomes.js +348 -0
- package/lib/notification-outcomes.js.map +1 -0
- package/lib/service.d.ts +295 -2
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +965 -5
- package/lib/service.js.map +1 -1
- package/lib/sqlite.d.ts +8 -2
- package/lib/sqlite.d.ts.map +1 -1
- package/lib/sqlite.js +406 -17
- package/lib/sqlite.js.map +1 -1
- package/lib/store.d.ts +13 -2
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +122 -24
- package/lib/store.js.map +1 -1
- package/lib/types.d.ts +247 -5
- package/lib/types.d.ts.map +1 -1
- package/lib/types.js +11 -0
- package/lib/types.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/lib/work-item-owner-proof.d.ts +4 -0
- package/lib/work-item-owner-proof.d.ts.map +1 -0
- package/lib/work-item-owner-proof.js +53 -0
- package/lib/work-item-owner-proof.js.map +1 -0
- package/package.json +29 -38
- package/lib/benchmark/super-relay.d.ts +0 -26
- package/lib/benchmark/super-relay.d.ts.map +0 -1
- package/lib/benchmark/super-relay.js +0 -173
- package/lib/benchmark/super-relay.js.map +0 -1
package/lib/service.js
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
import { Context, Service, symbols } from '@deepseek-ai/cordis';
|
|
2
2
|
import Schema from '@deepseek-ai/schemastery';
|
|
3
3
|
import { acceptanceDigest, goalDefinitionSituation, validateTaskAcceptanceContract, validateTaskVerificationReceipt } from '@dsh-enhanced/task-acceptance-contract';
|
|
4
|
-
import { EvaluationStore, canonicalEvaluationScope, evaluationLearningProjectionDigest, } from './store.js';
|
|
4
|
+
import { EvaluationStore, EvaluationStoreError, canonicalEvaluationScope, evaluationLearningProjectionDigest, } from './store.js';
|
|
5
5
|
import { registerEvaluationTools } from './tools.js';
|
|
6
|
+
import { normalizeAttentionCohort, } from './attention-tuning.js';
|
|
7
|
+
import { ATTENTION_OWNER_QUESTION_SOURCE_ID, attentionOwnerQuestionIdempotencyKey, attentionOwnerQuestionText, parseAttentionOwnerQuestionChoice, ATTENTION_OWNER_QUESTION_REPLY_PROTOCOL, } from './attention-owner-questions.js';
|
|
8
|
+
import { validateWorkItemOwnerFeedbackProof } from './work-item-owner-proof.js';
|
|
9
|
+
import { computeIntakeAttentionAgreement, INTAKE_ATTENTION_AGREEMENT_SCAN_LIMIT, isIntakeProvenanceSource } from './intake-attention-agreement.js';
|
|
6
10
|
import { executionStatuses, objectiveStatuses, outcomeSourceKinds, TRUSTED_EVALUATION_PRODUCER_PROTOCOL, } from './types.js';
|
|
11
|
+
const OWNER_NOTIFICATION_WITHDRAWAL_PROTOCOL = 'assistant-delivery/owner-notification-withdrawal/v1';
|
|
12
|
+
function isOwnerQuestionDeliveryPort(value) {
|
|
13
|
+
const port = value;
|
|
14
|
+
return typeof port === 'object' && port !== null && typeof port.enqueueOwnerNotification === 'function'
|
|
15
|
+
&& typeof port.validateOwnerRoute === 'function' && typeof port.resolveOwnerRoute === 'function'
|
|
16
|
+
&& typeof port.inspectOwnerNotification === 'function' && typeof port.registerOwnerNotificationWithdrawal === 'function';
|
|
17
|
+
}
|
|
18
|
+
function isOwnerQuestionReplyPort(value) {
|
|
19
|
+
const port = value;
|
|
20
|
+
return typeof port === 'object' && port !== null && typeof port.registerOwnerNotificationReply === 'function';
|
|
21
|
+
}
|
|
7
22
|
export const ASSISTANT_EVALUATION_SKILL = `# Personal assistant self-evaluation
|
|
8
23
|
|
|
9
24
|
Use this workflow only to assess whether an already-finished task met its objective. A self-assessment is diagnostic evidence, never ground truth and never permission to change production behavior.
|
|
@@ -34,6 +49,31 @@ const configSchema = Schema.object({
|
|
|
34
49
|
projectionTimeoutMs: Schema.number().step(1).min(100).max(300_000).default(2_000),
|
|
35
50
|
projectionRetryBaseMs: Schema.number().step(1).min(1).max(3_600_000).default(1_000),
|
|
36
51
|
projectionRetryMaxMs: Schema.number().step(1).min(1).max(86_400_000).default(3_600_000),
|
|
52
|
+
notificationOutcomesEnabled: Schema.boolean().default(false),
|
|
53
|
+
notificationResponseWindowMs: Schema.number().step(1).min(60_000).max(2_592_000_000).default(86_400_000),
|
|
54
|
+
notificationObservationHorizonMs: Schema.number().step(1).min(60_000).max(31_536_000_000).default(2_592_000_000),
|
|
55
|
+
notificationSyncBatchSize: Schema.number().step(1).min(1).max(500).default(100),
|
|
56
|
+
notificationSyncIntervalMs: Schema.number().step(1).min(0).max(86_400_000).default(60_000),
|
|
57
|
+
attentionTuningAutoAdopt: Schema.boolean().default(false),
|
|
58
|
+
intakeAttentionAgreementEnabled: Schema.boolean().default(false),
|
|
59
|
+
attentionTuningCohorts: Schema.array(Schema.object({
|
|
60
|
+
lane: Schema.string().required(),
|
|
61
|
+
sourceId: Schema.string().required(),
|
|
62
|
+
})).default([{ lane: 'owner-notification', sourceId: 'assistant-proactive/v1' }]),
|
|
63
|
+
attentionTuningMinSample: Schema.number().step(1).min(5).max(10_000).default(20),
|
|
64
|
+
attentionTuningNegativeRatePpm: Schema.number().step(1).min(100_000).max(1_000_000).default(600_000),
|
|
65
|
+
attentionTuningMuteRatePpm: Schema.number().step(1).min(100_000).max(1_000_000).default(900_000),
|
|
66
|
+
attentionTuningRegressionMarginPpm: Schema.number().step(1).min(0).max(1_000_000).default(100_000),
|
|
67
|
+
attentionTuningObservationMs: Schema.number().step(1).min(60_000).max(31_536_000_000).default(604_800_000),
|
|
68
|
+
attentionTuningEvaluationWindowMs: Schema.number().step(1).min(60_000).max(31_536_000_000).default(604_800_000),
|
|
69
|
+
attentionLooseningEnabled: Schema.boolean().default(false),
|
|
70
|
+
attentionLooseningReplayWindowMs: Schema.number().step(1).min(3_600_000).max(31_536_000_000).default(2_592_000_000),
|
|
71
|
+
attentionOwnerQuestionDeliveryEnabled: Schema.boolean().default(false),
|
|
72
|
+
attentionOwnerQuestionOwner: Schema.union([Schema.object({
|
|
73
|
+
ownerRouteId: Schema.string().required(),
|
|
74
|
+
principalId: Schema.string().required(),
|
|
75
|
+
})]),
|
|
76
|
+
attentionOwnerQuestionNoticeTtlMs: Schema.number().step(1).min(60_000).max(2_592_000_000).default(86_400_000),
|
|
37
77
|
});
|
|
38
78
|
export class AssistantEvaluationError extends Error {
|
|
39
79
|
code;
|
|
@@ -59,6 +99,10 @@ export function canonicalEvaluationHostScope(input) {
|
|
|
59
99
|
});
|
|
60
100
|
return Object.freeze(branded);
|
|
61
101
|
}
|
|
102
|
+
function isNotificationOutcomeFactSource(value) {
|
|
103
|
+
return typeof value === 'object' && value !== null
|
|
104
|
+
&& typeof value.inspectNotificationOutcomeFacts === 'function';
|
|
105
|
+
}
|
|
62
106
|
function exactEvaluationHostScope(input) {
|
|
63
107
|
if (typeof input !== 'object' || input === null || !Object.isFrozen(input)
|
|
64
108
|
|| input[evaluationHostScopeBrand] !== true) {
|
|
@@ -173,6 +217,11 @@ function isTrustedVerifierEvaluationProducer(value) {
|
|
|
173
217
|
&& typeof value.trustedVerificationProducerGeneration === 'function'
|
|
174
218
|
&& typeof value.registerTrustedVerifierEvaluationSink === 'function';
|
|
175
219
|
}
|
|
220
|
+
function isTrustedWorkItemEvaluationProducer(value) {
|
|
221
|
+
return typeof value === 'object' && value !== null
|
|
222
|
+
&& typeof value.trustedWorkItemProducerGeneration === 'function'
|
|
223
|
+
&& typeof value.registerTrustedWorkItemEvaluationSink === 'function';
|
|
224
|
+
}
|
|
176
225
|
function isTrustedEvaluationProjector(value) {
|
|
177
226
|
return typeof value === 'object' && value !== null
|
|
178
227
|
&& typeof value.projectTrustedEvaluationTaskRevision === 'function';
|
|
@@ -194,11 +243,20 @@ export class AssistantEvaluationService extends Service {
|
|
|
194
243
|
automationBinding;
|
|
195
244
|
deliveryBinding;
|
|
196
245
|
verifierBinding;
|
|
246
|
+
workItemsBinding;
|
|
197
247
|
activeAutomationRegistrations = new WeakSet();
|
|
198
248
|
activeDeliveryRegistrations = new WeakSet();
|
|
199
249
|
activeVerifierRegistrations = new WeakSet();
|
|
250
|
+
activeWorkItemRegistrations = new WeakSet();
|
|
200
251
|
activeProjections = new Map();
|
|
201
252
|
projectionTimer;
|
|
253
|
+
notificationFacts;
|
|
254
|
+
lastNotificationSyncAt = Number.NEGATIVE_INFINITY;
|
|
255
|
+
ownerQuestionWithdrawal;
|
|
256
|
+
activeOwnerQuestionWithdrawals = new WeakSet();
|
|
257
|
+
ownerQuestionReply;
|
|
258
|
+
activeOwnerQuestionReplies = new WeakSet();
|
|
259
|
+
attentionCohorts;
|
|
202
260
|
active = true;
|
|
203
261
|
constructor(ctx, input, options = {}) {
|
|
204
262
|
super(ctx, 'assistantEvaluation');
|
|
@@ -217,6 +275,30 @@ export class AssistantEvaluationService extends Service {
|
|
|
217
275
|
if (this.config.projectionRetryBaseMs > this.config.projectionRetryMaxMs) {
|
|
218
276
|
throw new Error('assistant-evaluation: projectionRetryBaseMs must not exceed projectionRetryMaxMs');
|
|
219
277
|
}
|
|
278
|
+
if (this.config.intakeAttentionAgreementEnabled && !this.config.notificationOutcomesEnabled) {
|
|
279
|
+
throw new Error('assistant-evaluation: intakeAttentionAgreementEnabled requires notificationOutcomesEnabled');
|
|
280
|
+
}
|
|
281
|
+
if (this.config.attentionTuningAutoAdopt && !this.config.notificationOutcomesEnabled) {
|
|
282
|
+
throw new Error('assistant-evaluation: attentionTuningAutoAdopt requires notificationOutcomesEnabled');
|
|
283
|
+
}
|
|
284
|
+
if (this.config.attentionLooseningEnabled && !this.config.attentionTuningAutoAdopt) {
|
|
285
|
+
throw new Error('assistant-evaluation: attentionLooseningEnabled requires attentionTuningAutoAdopt');
|
|
286
|
+
}
|
|
287
|
+
if (this.config.attentionOwnerQuestionDeliveryEnabled && !this.config.attentionLooseningEnabled) {
|
|
288
|
+
throw new Error('assistant-evaluation: attentionOwnerQuestionDeliveryEnabled requires attentionLooseningEnabled');
|
|
289
|
+
}
|
|
290
|
+
if (this.config.attentionOwnerQuestionDeliveryEnabled && (typeof this.config.attentionOwnerQuestionOwner?.ownerRouteId !== 'string'
|
|
291
|
+
|| this.config.attentionOwnerQuestionOwner.ownerRouteId.length === 0 || typeof this.config.attentionOwnerQuestionOwner.principalId !== 'string'
|
|
292
|
+
|| this.config.attentionOwnerQuestionOwner.principalId.length === 0)) {
|
|
293
|
+
throw new Error('assistant-evaluation: attentionOwnerQuestionDeliveryEnabled requires attentionOwnerQuestionOwner');
|
|
294
|
+
}
|
|
295
|
+
if (this.config.attentionTuningMuteRatePpm < this.config.attentionTuningNegativeRatePpm) {
|
|
296
|
+
throw new Error('assistant-evaluation: attentionTuningMuteRatePpm must not be below attentionTuningNegativeRatePpm');
|
|
297
|
+
}
|
|
298
|
+
if (this.config.attentionTuningCohorts.length > 16) {
|
|
299
|
+
throw new Error('assistant-evaluation: at most 16 attention tuning cohorts');
|
|
300
|
+
}
|
|
301
|
+
this.attentionCohorts = Object.freeze(this.config.attentionTuningCohorts.map(cohort => normalizeAttentionCohort(cohort)));
|
|
220
302
|
this.now = options.now ?? Date.now;
|
|
221
303
|
this.store = new EvaluationStore({
|
|
222
304
|
path: this.config.databasePath,
|
|
@@ -313,6 +395,21 @@ export class AssistantEvaluationService extends Service {
|
|
|
313
395
|
return;
|
|
314
396
|
return this.bindDeliveryProducer(producer);
|
|
315
397
|
});
|
|
398
|
+
const bindNotificationFacts = (source) => {
|
|
399
|
+
if (!isNotificationOutcomeFactSource(source))
|
|
400
|
+
return;
|
|
401
|
+
this.notificationFacts = source;
|
|
402
|
+
return () => { if (this.notificationFacts === source)
|
|
403
|
+
this.notificationFacts = undefined; };
|
|
404
|
+
};
|
|
405
|
+
bindNotificationFacts(currentDelivery);
|
|
406
|
+
ctx.inject(['assistantDelivery'], deliveryCtx => bindNotificationFacts(deliveryCtx.get('assistantDelivery')));
|
|
407
|
+
if (this.config.attentionOwnerQuestionDeliveryEnabled) {
|
|
408
|
+
this.bindOwnerQuestionWithdrawal(currentDelivery);
|
|
409
|
+
ctx.inject(['assistantDelivery'], deliveryCtx => this.bindOwnerQuestionWithdrawal(deliveryCtx.get('assistantDelivery')));
|
|
410
|
+
this.bindOwnerQuestionReply(currentDelivery);
|
|
411
|
+
ctx.inject(['assistantDelivery'], deliveryCtx => this.bindOwnerQuestionReply(deliveryCtx.get('assistantDelivery')));
|
|
412
|
+
}
|
|
316
413
|
const currentVerifier = ctx.get('assistantVerifier');
|
|
317
414
|
if (isTrustedVerifierEvaluationProducer(currentVerifier))
|
|
318
415
|
this.bindVerifierProducer(currentVerifier);
|
|
@@ -322,8 +419,33 @@ export class AssistantEvaluationService extends Service {
|
|
|
322
419
|
return;
|
|
323
420
|
return this.bindVerifierProducer(producer);
|
|
324
421
|
});
|
|
422
|
+
const currentWorkItems = ctx.get('assistantWorkItems');
|
|
423
|
+
if (isTrustedWorkItemEvaluationProducer(currentWorkItems))
|
|
424
|
+
this.bindWorkItemsProducer(currentWorkItems);
|
|
425
|
+
ctx.inject(['assistantWorkItems'], workItemsCtx => {
|
|
426
|
+
const producer = workItemsCtx.get('assistantWorkItems');
|
|
427
|
+
if (!isTrustedWorkItemEvaluationProducer(producer))
|
|
428
|
+
return;
|
|
429
|
+
return this.bindWorkItemsProducer(producer);
|
|
430
|
+
});
|
|
325
431
|
if (this.config.projectionIntervalMs > 0) {
|
|
326
432
|
this.projectionTimer = setInterval(() => {
|
|
433
|
+
const now = this.now();
|
|
434
|
+
if (this.config.notificationOutcomesEnabled && now - this.lastNotificationSyncAt >= this.config.notificationSyncIntervalMs) {
|
|
435
|
+
this.lastNotificationSyncAt = now;
|
|
436
|
+
try {
|
|
437
|
+
this.reconcileNotificationOutcomes();
|
|
438
|
+
}
|
|
439
|
+
catch { /* isolated from projection; retried on a later tick */ }
|
|
440
|
+
try {
|
|
441
|
+
this.reconcileAttentionTuning();
|
|
442
|
+
}
|
|
443
|
+
catch { /* retried on a later sync */ }
|
|
444
|
+
try {
|
|
445
|
+
this.reconcileAttentionOwnerQuestions();
|
|
446
|
+
}
|
|
447
|
+
catch { /* retried on a later sync */ }
|
|
448
|
+
}
|
|
327
449
|
void this.reconcileProjections().catch(() => { });
|
|
328
450
|
}, this.config.projectionIntervalMs);
|
|
329
451
|
this.projectionTimer.unref?.();
|
|
@@ -333,10 +455,14 @@ export class AssistantEvaluationService extends Service {
|
|
|
333
455
|
if (this.projectionTimer !== undefined)
|
|
334
456
|
clearInterval(this.projectionTimer);
|
|
335
457
|
this.projectionTimer = undefined;
|
|
458
|
+
this.notificationFacts = undefined;
|
|
459
|
+
this.ownerQuestionWithdrawal?.dispose();
|
|
460
|
+
this.ownerQuestionReply?.dispose();
|
|
336
461
|
disposeCurrentProjector?.();
|
|
337
462
|
this.automationBinding?.dispose();
|
|
338
463
|
this.deliveryBinding?.dispose();
|
|
339
464
|
this.verifierBinding?.dispose();
|
|
465
|
+
this.workItemsBinding?.dispose();
|
|
340
466
|
toolsBinding?.dispose();
|
|
341
467
|
skillBinding?.dispose();
|
|
342
468
|
await Promise.allSettled(this.activeProjections.values());
|
|
@@ -412,8 +538,8 @@ export class AssistantEvaluationService extends Service {
|
|
|
412
538
|
if (state === undefined) {
|
|
413
539
|
throw new AssistantEvaluationError('not-found', 'exact Evaluation projection was not found');
|
|
414
540
|
}
|
|
415
|
-
if (state.status
|
|
416
|
-
return Object.freeze({ evaluationId, status:
|
|
541
|
+
if (state.status !== 'pending') {
|
|
542
|
+
return Object.freeze({ evaluationId, status: state.status, attemptCount: state.attemptCount });
|
|
417
543
|
}
|
|
418
544
|
const active = this.activeProjections.get(evaluationId);
|
|
419
545
|
if (active !== undefined)
|
|
@@ -434,7 +560,7 @@ export class AssistantEvaluationService extends Service {
|
|
|
434
560
|
}
|
|
435
561
|
return Object.freeze({
|
|
436
562
|
evaluationId,
|
|
437
|
-
status: state.status === '
|
|
563
|
+
status: state.status === 'pending' ? 'deferred' : state.status,
|
|
438
564
|
attemptCount: state.attemptCount,
|
|
439
565
|
});
|
|
440
566
|
}
|
|
@@ -513,6 +639,11 @@ export class AssistantEvaluationService extends Service {
|
|
|
513
639
|
this.assertActive();
|
|
514
640
|
return this.store.getForegroundLearningProjection(exactEvaluationHostScope(input.scope), hostIdentifier(input.inboxId, 'inboxId', 1_000));
|
|
515
641
|
}
|
|
642
|
+
/** Host-only exact current owner result for one durable work item. */
|
|
643
|
+
getTrustedWorkItemLearningProjection(input) {
|
|
644
|
+
this.assertActive();
|
|
645
|
+
return this.store.getWorkItemLearningProjection(exactEvaluationHostScope(input.scope), hostIdentifier(input.itemId, 'itemId', 1_000));
|
|
646
|
+
}
|
|
516
647
|
/** Host-only exact canonical whole-goal proof, addressed by assessment id. */
|
|
517
648
|
getTrustedGoalOutcomeLearningProjection(input) {
|
|
518
649
|
this.assertActive();
|
|
@@ -544,6 +675,7 @@ export class AssistantEvaluationService extends Service {
|
|
|
544
675
|
&& projection.subjectKind !== 'foreground-turn'
|
|
545
676
|
&& projection.subjectKind !== 'goal-step'
|
|
546
677
|
&& projection.subjectKind !== 'goal-outcome'
|
|
678
|
+
&& projection.subjectKind !== 'work-item'
|
|
547
679
|
&& projection.subjectKind !== 'outcome')
|
|
548
680
|
|| !exactHostIdentifier(projection.subjectRef, 'projection.subjectRef', 1_000)
|
|
549
681
|
|| !Number.isSafeInteger(projection.version) || projection.version < 1
|
|
@@ -700,6 +832,578 @@ export class AssistantEvaluationService extends Service {
|
|
|
700
832
|
this.assertActive();
|
|
701
833
|
return this.store.health();
|
|
702
834
|
}
|
|
835
|
+
/**
|
|
836
|
+
* WP22 deterministic sync: ingest new Delivery audit rows after the durable
|
|
837
|
+
* cursor, then refresh a bounded batch of known notifications inside the
|
|
838
|
+
* observation horizon (least recently checked first) so late replies,
|
|
839
|
+
* corrections, withdrawals and expiry produce new revisions. No model call,
|
|
840
|
+
* no Evolution projection and no parameter change. Missing Delivery is a
|
|
841
|
+
* zero result, as is a disabled `notificationOutcomesEnabled`.
|
|
842
|
+
*/
|
|
843
|
+
reconcileNotificationOutcomes() {
|
|
844
|
+
this.assertActive();
|
|
845
|
+
const source = this.notificationFacts;
|
|
846
|
+
if (!this.config.notificationOutcomesEnabled || source === undefined)
|
|
847
|
+
return Object.freeze({ ingested: 0, refreshed: 0, revised: 0, invalid: 0 });
|
|
848
|
+
const ledger = this.store.notifications;
|
|
849
|
+
const limit = this.config.notificationSyncBatchSize;
|
|
850
|
+
const window = this.config.notificationResponseWindowMs;
|
|
851
|
+
let ingested = 0;
|
|
852
|
+
let refreshed = 0;
|
|
853
|
+
let revised = 0;
|
|
854
|
+
let invalid = 0;
|
|
855
|
+
const apply = (fact) => {
|
|
856
|
+
try {
|
|
857
|
+
if (ledger.apply(fact, window).revised)
|
|
858
|
+
revised += 1;
|
|
859
|
+
return true;
|
|
860
|
+
}
|
|
861
|
+
catch (error) {
|
|
862
|
+
// A malformed fact or a fact whose scope disagrees with its recorded
|
|
863
|
+
// head is counted and skipped so one bad row never stalls the cursor.
|
|
864
|
+
if (error instanceof EvaluationStoreError && (error.code === 'invalid-input' || error.code === 'version-conflict')) {
|
|
865
|
+
invalid += 1;
|
|
866
|
+
return false;
|
|
867
|
+
}
|
|
868
|
+
throw error;
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
const page = source.inspectNotificationOutcomeFacts({ afterSequence: ledger.syncCursor(), limit });
|
|
872
|
+
for (const fact of page.facts)
|
|
873
|
+
if (apply(fact))
|
|
874
|
+
ingested += 1;
|
|
875
|
+
if (page.nextAfterSequence !== null)
|
|
876
|
+
ledger.advanceSyncCursor(page.nextAfterSequence);
|
|
877
|
+
// The horizon is measured from notification time while expiry is measured
|
|
878
|
+
// from send time, so a horizon near the response window would stop
|
|
879
|
+
// re-checking a delivered notification before it could expire. Keep at
|
|
880
|
+
// least one extra window of slack for send latency.
|
|
881
|
+
const horizon = Math.max(this.config.notificationObservationHorizonMs, 2 * window);
|
|
882
|
+
const due = ledger.dueForRefresh(this.now() - horizon, limit);
|
|
883
|
+
if (due.length > 0) {
|
|
884
|
+
const facts = source.inspectNotificationOutcomeFacts({ outboxIds: due }).facts;
|
|
885
|
+
const applied = new Set();
|
|
886
|
+
for (const fact of facts) {
|
|
887
|
+
if (!apply(fact))
|
|
888
|
+
continue;
|
|
889
|
+
refreshed += 1;
|
|
890
|
+
applied.add(fact.outboxId);
|
|
891
|
+
}
|
|
892
|
+
// Missing or rejected facts still rotate to the back of the re-check
|
|
893
|
+
// order so they cannot starve the rest of the horizon.
|
|
894
|
+
for (const id of due)
|
|
895
|
+
if (!applied.has(id))
|
|
896
|
+
ledger.touch(id);
|
|
897
|
+
}
|
|
898
|
+
return Object.freeze({ ingested, refreshed, revised, invalid });
|
|
899
|
+
}
|
|
900
|
+
/** Current revision plus full revision history for one notification (Delivery Outbox id). */
|
|
901
|
+
inspectNotificationOutcome(input) {
|
|
902
|
+
this.assertActive();
|
|
903
|
+
return this.store.notifications.history(input.scope, hostIdentifier(input.notificationId, 'notificationId', 256));
|
|
904
|
+
}
|
|
905
|
+
/** Restart-safe feed of current notification heads after a per-scope notification watermark. */
|
|
906
|
+
notificationOutcomeFeed(input) {
|
|
907
|
+
this.assertActive();
|
|
908
|
+
return this.store.notifications.feed(input.scope, input.after, input.limit ?? 100);
|
|
909
|
+
}
|
|
910
|
+
/** Current-head counts by outcome and by attention verdict, over notification time. */
|
|
911
|
+
notificationOutcomeSummary(input) {
|
|
912
|
+
this.assertActive();
|
|
913
|
+
const to = input.toNotifiedAt ?? this.now() + 1;
|
|
914
|
+
const from = input.fromNotifiedAt ?? Math.max(0, to - this.config.defaultSummaryWindowMs);
|
|
915
|
+
if (!Number.isSafeInteger(from) || !Number.isSafeInteger(to) || from < 0 || to <= from
|
|
916
|
+
|| to - from > this.config.maxSummaryWindowMs) {
|
|
917
|
+
throw new AssistantEvaluationError('invalid-input', 'notification outcome summary window is invalid');
|
|
918
|
+
}
|
|
919
|
+
return this.store.notifications.summary(input.scope, from, to);
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* WP22, read-only and default off: agreement between the WP20 intake
|
|
923
|
+
* judgment behind each auto-admitted work item and the owner outcome of its
|
|
924
|
+
* notifications, over notification time. Undefined when
|
|
925
|
+
* `intakeAttentionAgreementEnabled` is off. Observe-mode intake creates no
|
|
926
|
+
* item and is therefore not measurable here. Observational (`causal:
|
|
927
|
+
* false`); it writes, adopts and sends nothing.
|
|
928
|
+
*/
|
|
929
|
+
inspectIntakeAttentionAgreement(input) {
|
|
930
|
+
this.assertActive();
|
|
931
|
+
if (!this.config.intakeAttentionAgreementEnabled)
|
|
932
|
+
return undefined;
|
|
933
|
+
const to = input.toNotifiedAt ?? this.now() + 1;
|
|
934
|
+
const from = input.fromNotifiedAt ?? Math.max(0, to - this.config.defaultSummaryWindowMs);
|
|
935
|
+
if (!Number.isSafeInteger(from) || !Number.isSafeInteger(to) || from < 0 || to <= from
|
|
936
|
+
|| to - from > this.config.maxSummaryWindowMs) {
|
|
937
|
+
throw new AssistantEvaluationError('invalid-input', 'intake attention agreement window is invalid');
|
|
938
|
+
}
|
|
939
|
+
const { scope } = canonicalEvaluationScope(input.scope);
|
|
940
|
+
const heads = this.store.notifications.workItemHeads(scope, from, to, INTAKE_ATTENTION_AGREEMENT_SCAN_LIMIT);
|
|
941
|
+
const workItems = this.ctx.get('assistantWorkItems');
|
|
942
|
+
return computeIntakeAttentionAgreement({ scope, fromNotifiedAt: from, toNotifiedAt: to, records: heads.records,
|
|
943
|
+
truncated: heads.truncated, source: isIntakeProvenanceSource(workItems) ? workItems : undefined });
|
|
944
|
+
}
|
|
945
|
+
#attentionPolicy() {
|
|
946
|
+
return Object.freeze({
|
|
947
|
+
minSample: this.config.attentionTuningMinSample,
|
|
948
|
+
negativeRatePpm: this.config.attentionTuningNegativeRatePpm,
|
|
949
|
+
muteRatePpm: this.config.attentionTuningMuteRatePpm,
|
|
950
|
+
regressionMarginPpm: this.config.attentionTuningRegressionMarginPpm,
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
#looseningOptions() {
|
|
954
|
+
if (!this.config.attentionLooseningEnabled)
|
|
955
|
+
return undefined;
|
|
956
|
+
return Object.freeze({
|
|
957
|
+
replayWindowMs: this.config.attentionLooseningReplayWindowMs,
|
|
958
|
+
...(this.config.attentionOwnerQuestionDeliveryEnabled
|
|
959
|
+
? { ownerQuestionReplyLoop: Object.freeze({ noticeTtlMs: this.config.attentionOwnerQuestionNoticeTtlMs }) }
|
|
960
|
+
: {}),
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
#attentionCohort(input) {
|
|
964
|
+
let cohort;
|
|
965
|
+
try {
|
|
966
|
+
cohort = normalizeAttentionCohort(input);
|
|
967
|
+
}
|
|
968
|
+
catch {
|
|
969
|
+
throw new AssistantEvaluationError('invalid-input', 'attention cohort is invalid');
|
|
970
|
+
}
|
|
971
|
+
if (!this.attentionCohorts.some(item => item.lane === cohort.lane && item.sourceId === cohort.sourceId)) {
|
|
972
|
+
throw new AssistantEvaluationError('not-found', 'attention cohort is not configured');
|
|
973
|
+
}
|
|
974
|
+
return cohort;
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* WP22 slice 2 deterministic tuning tick: for every configured cohort and
|
|
978
|
+
* every scope that has notifications in it, monitor an observing version
|
|
979
|
+
* (exact-parent rollback on regression) or adopt one bounded tightening
|
|
980
|
+
* step. With `attentionLooseningEnabled`, a pending sealed loosening
|
|
981
|
+
* proposal is decided first and tightening uses training data only.
|
|
982
|
+
* Zero work when `attentionTuningAutoAdopt` is off.
|
|
983
|
+
*/
|
|
984
|
+
reconcileAttentionTuning() {
|
|
985
|
+
this.assertActive();
|
|
986
|
+
if (!this.config.attentionTuningAutoAdopt)
|
|
987
|
+
return Object.freeze([]);
|
|
988
|
+
const policy = this.#attentionPolicy();
|
|
989
|
+
const loosening = this.#looseningOptions();
|
|
990
|
+
const options = {
|
|
991
|
+
observationMs: this.config.attentionTuningObservationMs, evaluationWindowMs: this.config.attentionTuningEvaluationWindowMs,
|
|
992
|
+
...(loosening === undefined ? {} : { loosening }),
|
|
993
|
+
};
|
|
994
|
+
const results = [];
|
|
995
|
+
for (const cohort of this.attentionCohorts) {
|
|
996
|
+
for (const scope of this.store.attention.scopes(cohort)) {
|
|
997
|
+
let scopeKey;
|
|
998
|
+
try {
|
|
999
|
+
scopeKey = canonicalEvaluationScope(scope).scopeKey;
|
|
1000
|
+
}
|
|
1001
|
+
catch {
|
|
1002
|
+
continue;
|
|
1003
|
+
}
|
|
1004
|
+
results.push({ scopeKey, cohort, result: this.store.attention.tick(scope, cohort, policy, options) });
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
return Object.freeze(results);
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Verified active attention parameters for a consumer, or undefined when
|
|
1011
|
+
* auto-adoption is off (consumers then apply no overlay at all).
|
|
1012
|
+
*/
|
|
1013
|
+
currentAttentionTuning(input) {
|
|
1014
|
+
this.assertActive();
|
|
1015
|
+
if (!this.config.attentionTuningAutoAdopt)
|
|
1016
|
+
return undefined;
|
|
1017
|
+
return this.store.attention.current(input.scope, this.#attentionCohort(input.cohort));
|
|
1018
|
+
}
|
|
1019
|
+
/** Versions, durable observation state and recent events for one cohort. */
|
|
1020
|
+
attentionTuningStatus(input) {
|
|
1021
|
+
this.assertActive();
|
|
1022
|
+
return this.store.attention.status(input.scope, this.#attentionCohort(input.cohort));
|
|
1023
|
+
}
|
|
1024
|
+
/**
|
|
1025
|
+
* Host-internal explicit request. Without `attentionLooseningEnabled`,
|
|
1026
|
+
* loosening is rejected as `loosening-requires-holdout` and recorded; with
|
|
1027
|
+
* it, a single-parameter loosening is sealed as a proposal for the next
|
|
1028
|
+
* tick's holdout replay and grant check.
|
|
1029
|
+
*/
|
|
1030
|
+
requestAttentionTuning(input) {
|
|
1031
|
+
this.assertActive();
|
|
1032
|
+
return this.store.attention.request(input.scope, this.#attentionCohort(input.cohort), input.parameters, {
|
|
1033
|
+
enabled: this.config.attentionTuningAutoAdopt,
|
|
1034
|
+
observationMs: this.config.attentionTuningObservationMs,
|
|
1035
|
+
evaluationWindowMs: this.config.attentionTuningEvaluationWindowMs,
|
|
1036
|
+
...(this.#looseningOptions() === undefined ? {} : { loosening: this.#looseningOptions() }),
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
/** Record an owner one-time loosening pre-authorization for one cohort. */
|
|
1040
|
+
grantAttentionLoosening(input) {
|
|
1041
|
+
this.assertActive();
|
|
1042
|
+
const cohort = this.#attentionCohort(input.cohort);
|
|
1043
|
+
if (!this.config.attentionLooseningEnabled)
|
|
1044
|
+
return Object.freeze({ status: 'rejected', reason: 'loosening-disabled' });
|
|
1045
|
+
return this.store.attention.grant(input.scope, cohort, input.grant);
|
|
1046
|
+
}
|
|
1047
|
+
/** Active grant with used/remaining quota, sealed proposals with decisions, and recorded owner questions. */
|
|
1048
|
+
attentionLooseningStatus(input) {
|
|
1049
|
+
this.assertActive();
|
|
1050
|
+
const status = this.store.attention.looseningStatus(input.scope, this.#attentionCohort(input.cohort));
|
|
1051
|
+
return Object.freeze({ enabled: this.config.attentionLooseningEnabled, ...status });
|
|
1052
|
+
}
|
|
1053
|
+
#ownerQuestionDelivery() {
|
|
1054
|
+
const delivery = this.ctx.get('assistantDelivery');
|
|
1055
|
+
return isOwnerQuestionDeliveryPort(delivery) ? delivery : undefined;
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* Register (once per Delivery instance) the send-time withdrawal check for
|
|
1059
|
+
* owner-question notices. A Delivery without the check is treated as absent.
|
|
1060
|
+
*/
|
|
1061
|
+
bindOwnerQuestionWithdrawal(source) {
|
|
1062
|
+
if (!this.active || !isOwnerQuestionDeliveryPort(source))
|
|
1063
|
+
return undefined;
|
|
1064
|
+
if (this.ownerQuestionWithdrawal?.source === source)
|
|
1065
|
+
return undefined;
|
|
1066
|
+
const registration = Object.freeze({
|
|
1067
|
+
protocol: OWNER_NOTIFICATION_WITHDRAWAL_PROTOCOL,
|
|
1068
|
+
authority: 'assistantEvaluation',
|
|
1069
|
+
sourceId: ATTENTION_OWNER_QUESTION_SOURCE_ID,
|
|
1070
|
+
isWithdrawn: (idempotencyKey) => this.#ownerQuestionWithdrawn(idempotencyKey),
|
|
1071
|
+
});
|
|
1072
|
+
this.activeOwnerQuestionWithdrawals.add(registration);
|
|
1073
|
+
let handle;
|
|
1074
|
+
let unregister;
|
|
1075
|
+
try {
|
|
1076
|
+
const result = source.registerOwnerNotificationWithdrawal(registration);
|
|
1077
|
+
handle = result?.handle;
|
|
1078
|
+
unregister = result?.unregister;
|
|
1079
|
+
}
|
|
1080
|
+
catch (error) {
|
|
1081
|
+
this.activeOwnerQuestionWithdrawals.delete(registration);
|
|
1082
|
+
// Unbound: the tick defers owner questions as delivery-unavailable until a later bind succeeds.
|
|
1083
|
+
this.ctx.logger.warn(`assistant-evaluation: owner-question withdrawal registration failed; owner questions deferred: ${String(error)}`);
|
|
1084
|
+
return undefined;
|
|
1085
|
+
}
|
|
1086
|
+
if (typeof handle !== 'string' || handle.length === 0 || typeof unregister !== 'function') {
|
|
1087
|
+
this.activeOwnerQuestionWithdrawals.delete(registration);
|
|
1088
|
+
try {
|
|
1089
|
+
if (typeof unregister === 'function')
|
|
1090
|
+
unregister();
|
|
1091
|
+
}
|
|
1092
|
+
catch { /* best effort */ }
|
|
1093
|
+
this.ctx.logger.warn('assistant-evaluation: owner-question withdrawal registration returned no handle; owner questions deferred');
|
|
1094
|
+
return undefined;
|
|
1095
|
+
}
|
|
1096
|
+
const release = unregister;
|
|
1097
|
+
this.ownerQuestionWithdrawal?.dispose();
|
|
1098
|
+
let live = true;
|
|
1099
|
+
const binding = Object.freeze({ source, handle, dispose: () => {
|
|
1100
|
+
if (!live)
|
|
1101
|
+
return;
|
|
1102
|
+
live = false;
|
|
1103
|
+
this.activeOwnerQuestionWithdrawals.delete(registration);
|
|
1104
|
+
try {
|
|
1105
|
+
release();
|
|
1106
|
+
}
|
|
1107
|
+
catch { /* Delivery already gone */ }
|
|
1108
|
+
if (this.ownerQuestionWithdrawal === binding)
|
|
1109
|
+
this.ownerQuestionWithdrawal = undefined;
|
|
1110
|
+
} });
|
|
1111
|
+
this.ownerQuestionWithdrawal = binding;
|
|
1112
|
+
return binding.dispose;
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Register (once per Delivery instance) the slice-5 reply consumer. A
|
|
1116
|
+
* Delivery without the reply port is treated as absent: slice-4 notice
|
|
1117
|
+
* delivery keeps working, only replies are not read. A registration error
|
|
1118
|
+
* only disables replies; it never blocks notice delivery.
|
|
1119
|
+
*/
|
|
1120
|
+
bindOwnerQuestionReply(source) {
|
|
1121
|
+
if (!this.active || !isOwnerQuestionReplyPort(source))
|
|
1122
|
+
return undefined;
|
|
1123
|
+
if (this.ownerQuestionReply?.source === source)
|
|
1124
|
+
return undefined;
|
|
1125
|
+
const registration = Object.freeze({
|
|
1126
|
+
protocol: ATTENTION_OWNER_QUESTION_REPLY_PROTOCOL,
|
|
1127
|
+
authority: 'assistantEvaluation',
|
|
1128
|
+
sourceId: ATTENTION_OWNER_QUESTION_SOURCE_ID,
|
|
1129
|
+
idempotencyKeyPrefix: attentionOwnerQuestionIdempotencyKey(''),
|
|
1130
|
+
consume: (input) => this.#consumeOwnerQuestionReply(input),
|
|
1131
|
+
});
|
|
1132
|
+
this.activeOwnerQuestionReplies.add(registration);
|
|
1133
|
+
let unregister;
|
|
1134
|
+
try {
|
|
1135
|
+
unregister = source.registerOwnerNotificationReply(registration)?.unregister;
|
|
1136
|
+
}
|
|
1137
|
+
catch (error) {
|
|
1138
|
+
this.activeOwnerQuestionReplies.delete(registration);
|
|
1139
|
+
this.ctx.logger.warn(`assistant-evaluation: owner-question reply registration failed; replies are not read: ${String(error)}`);
|
|
1140
|
+
return undefined;
|
|
1141
|
+
}
|
|
1142
|
+
if (typeof unregister !== 'function') {
|
|
1143
|
+
this.activeOwnerQuestionReplies.delete(registration);
|
|
1144
|
+
this.ctx.logger.warn('assistant-evaluation: owner-question reply registration returned no unregister; replies are not read');
|
|
1145
|
+
return undefined;
|
|
1146
|
+
}
|
|
1147
|
+
const release = unregister;
|
|
1148
|
+
this.ownerQuestionReply?.dispose();
|
|
1149
|
+
let live = true;
|
|
1150
|
+
const binding = Object.freeze({ source, registration, dispose: () => {
|
|
1151
|
+
if (!live)
|
|
1152
|
+
return;
|
|
1153
|
+
live = false;
|
|
1154
|
+
this.activeOwnerQuestionReplies.delete(registration);
|
|
1155
|
+
try {
|
|
1156
|
+
release();
|
|
1157
|
+
}
|
|
1158
|
+
catch { /* Delivery already gone */ }
|
|
1159
|
+
if (this.ownerQuestionReply === binding)
|
|
1160
|
+
this.ownerQuestionReply = undefined;
|
|
1161
|
+
} });
|
|
1162
|
+
this.ownerQuestionReply = binding;
|
|
1163
|
+
return binding.dispose;
|
|
1164
|
+
}
|
|
1165
|
+
/**
|
|
1166
|
+
* Synchronous Delivery callback for one authenticated reply to an exact
|
|
1167
|
+
* owner-question notice. Only exact whole-phrase choices are accepted; a
|
|
1168
|
+
* recognized choice is consumed even when its question is no longer open,
|
|
1169
|
+
* so a stale "authorize-once" never reaches the Agent. Storage errors are
|
|
1170
|
+
* `transient` and leave Delivery's Inbox pending for a bounded retry.
|
|
1171
|
+
*/
|
|
1172
|
+
#consumeOwnerQuestionReply(input) {
|
|
1173
|
+
if (!this.active || !this.config.attentionOwnerQuestionDeliveryEnabled)
|
|
1174
|
+
return 'ignored';
|
|
1175
|
+
if (typeof input?.idempotencyKey !== 'string' || typeof input.text !== 'string')
|
|
1176
|
+
return 'ignored';
|
|
1177
|
+
const prefix = attentionOwnerQuestionIdempotencyKey('');
|
|
1178
|
+
if (!input.idempotencyKey.startsWith(prefix))
|
|
1179
|
+
return 'ignored';
|
|
1180
|
+
const questionId = input.idempotencyKey.slice(prefix.length);
|
|
1181
|
+
const choice = parseAttentionOwnerQuestionChoice(input.text);
|
|
1182
|
+
if (choice === undefined || questionId.length === 0)
|
|
1183
|
+
return 'ignored';
|
|
1184
|
+
try {
|
|
1185
|
+
for (const cohort of this.attentionCohorts) {
|
|
1186
|
+
for (const scope of this.store.attention.scopes(cohort)) {
|
|
1187
|
+
const latest = this.store.attention.latestOwnerQuestion(scope, cohort);
|
|
1188
|
+
if (latest === undefined || latest === 'unverifiable' || latest.questionId !== questionId)
|
|
1189
|
+
continue;
|
|
1190
|
+
this.applyAttentionOwnerQuestionReply({ scope, cohort, questionId, choice });
|
|
1191
|
+
return 'applied';
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
// The notice was genuinely issued under this source and key, but the
|
|
1195
|
+
// local ledger no longer holds that exact question: consume the
|
|
1196
|
+
// recognized choice instead of sending it to the Agent.
|
|
1197
|
+
return 'applied';
|
|
1198
|
+
}
|
|
1199
|
+
catch {
|
|
1200
|
+
return 'transient';
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
/**
|
|
1204
|
+
* Send-time answer for one owner-question notice. It stays sendable only
|
|
1205
|
+
* while its question is still the cohort's latest, open, unanswered, inside
|
|
1206
|
+
* the notice TTL, and not recorded closed/expired. Anything else, including
|
|
1207
|
+
* a key this service did not issue, is withdrawn. A storage error throws so
|
|
1208
|
+
* Delivery holds the notice (bounded by its expiry) rather than sending.
|
|
1209
|
+
*/
|
|
1210
|
+
#ownerQuestionWithdrawn(idempotencyKey) {
|
|
1211
|
+
if (!this.active || !this.config.attentionOwnerQuestionDeliveryEnabled)
|
|
1212
|
+
return true;
|
|
1213
|
+
const prefix = attentionOwnerQuestionIdempotencyKey('');
|
|
1214
|
+
if (typeof idempotencyKey !== 'string' || !idempotencyKey.startsWith(prefix))
|
|
1215
|
+
return true;
|
|
1216
|
+
const questionId = idempotencyKey.slice(prefix.length);
|
|
1217
|
+
for (const cohort of this.attentionCohorts) {
|
|
1218
|
+
for (const scope of this.store.attention.scopes(cohort)) {
|
|
1219
|
+
const latest = this.store.attention.latestOwnerQuestion(scope, cohort);
|
|
1220
|
+
if (latest === undefined || latest === 'unverifiable' || latest.questionId !== questionId)
|
|
1221
|
+
continue;
|
|
1222
|
+
const delivery = this.store.attentionQuestions.delivery(questionId);
|
|
1223
|
+
return !latest.open || this.store.attentionQuestions.reply(questionId) !== undefined
|
|
1224
|
+
|| (delivery !== undefined && delivery.status !== 'queued')
|
|
1225
|
+
|| this.now() >= latest.question.createdAt + this.config.attentionOwnerQuestionNoticeTtlMs;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
return true;
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* WP22 slice 4 tick step: for each configured cohort and scope, the latest
|
|
1232
|
+
* recorded owner question is handed to Delivery at most once. An Outbox row
|
|
1233
|
+
* already holding the question's idempotency key is adopted as `queued`
|
|
1234
|
+
* before any other decision. Otherwise a question a newer grant (or a
|
|
1235
|
+
* recorded reply) already closed is recorded `closed` and never sent; one
|
|
1236
|
+
* older than the notice TTL is recorded `expired`. Notices are queued
|
|
1237
|
+
* `withdrawable`, so a question closed after queueing is withdrawn by
|
|
1238
|
+
* Delivery's send-time check instead of being sent. Delivery absence (or no
|
|
1239
|
+
* live withdrawal binding of this service to that Delivery instance), a
|
|
1240
|
+
* route that does not match the scope, or an enqueue failure leaves no row,
|
|
1241
|
+
* so a later tick retries under the same idempotency key. Zero work when
|
|
1242
|
+
* `attentionOwnerQuestionDeliveryEnabled` is off.
|
|
1243
|
+
*/
|
|
1244
|
+
reconcileAttentionOwnerQuestions() {
|
|
1245
|
+
this.assertActive();
|
|
1246
|
+
if (!this.config.attentionOwnerQuestionDeliveryEnabled)
|
|
1247
|
+
return Object.freeze([]);
|
|
1248
|
+
const owner = this.config.attentionOwnerQuestionOwner;
|
|
1249
|
+
const ttl = this.config.attentionOwnerQuestionNoticeTtlMs;
|
|
1250
|
+
const results = [];
|
|
1251
|
+
for (const cohort of this.attentionCohorts) {
|
|
1252
|
+
for (const scope of this.store.attention.scopes(cohort)) {
|
|
1253
|
+
let scopeKey;
|
|
1254
|
+
try {
|
|
1255
|
+
scopeKey = canonicalEvaluationScope(scope).scopeKey;
|
|
1256
|
+
}
|
|
1257
|
+
catch {
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
1260
|
+
const push = (result) => { results.push({ scopeKey, cohort, result: Object.freeze(result) }); };
|
|
1261
|
+
const latest = this.store.attention.latestOwnerQuestion(scope, cohort);
|
|
1262
|
+
if (latest === undefined)
|
|
1263
|
+
continue;
|
|
1264
|
+
// Fail closed: a question whose stored body does not verify is never sent.
|
|
1265
|
+
if (latest === 'unverifiable') {
|
|
1266
|
+
push({ action: 'unverifiable' });
|
|
1267
|
+
continue;
|
|
1268
|
+
}
|
|
1269
|
+
const { questionId, question } = latest;
|
|
1270
|
+
if (this.store.attentionQuestions.delivery(questionId) !== undefined)
|
|
1271
|
+
continue;
|
|
1272
|
+
const delivery = this.#ownerQuestionDelivery();
|
|
1273
|
+
// Without Delivery no Outbox lookup is possible, so no terminal row is written either.
|
|
1274
|
+
if (delivery === undefined) {
|
|
1275
|
+
push({ action: 'deferred', questionId, reason: 'delivery-unavailable' });
|
|
1276
|
+
continue;
|
|
1277
|
+
}
|
|
1278
|
+
this.bindOwnerQuestionWithdrawal(delivery);
|
|
1279
|
+
// A replaced Delivery instance needs the reply consumer bound as well;
|
|
1280
|
+
// the tick is the existing retry point, so no extra loop is added.
|
|
1281
|
+
this.bindOwnerQuestionReply(delivery);
|
|
1282
|
+
// Only queue (or adopt) while this service's own withdrawal check is
|
|
1283
|
+
// bound to this exact Delivery instance; otherwise a closed question
|
|
1284
|
+
// could be sent unchecked.
|
|
1285
|
+
const withdrawal = this.ownerQuestionWithdrawal;
|
|
1286
|
+
if (withdrawal === undefined || withdrawal.source !== delivery) {
|
|
1287
|
+
push({ action: 'deferred', questionId, reason: 'delivery-unavailable' });
|
|
1288
|
+
continue;
|
|
1289
|
+
}
|
|
1290
|
+
const withdrawalHandle = withdrawal.handle;
|
|
1291
|
+
const idempotencyKey = attentionOwnerQuestionIdempotencyKey(questionId);
|
|
1292
|
+
// An Outbox row under this key (e.g. enqueued before a crash, or under an
|
|
1293
|
+
// older TTL) is adopted as queued; the send-time withdrawal check then
|
|
1294
|
+
// keeps a since-closed question from being sent.
|
|
1295
|
+
let existing;
|
|
1296
|
+
try {
|
|
1297
|
+
existing = delivery.inspectOwnerNotification({ sourceId: ATTENTION_OWNER_QUESTION_SOURCE_ID, idempotencyKey, withdrawalHandle });
|
|
1298
|
+
}
|
|
1299
|
+
catch {
|
|
1300
|
+
push({ action: 'deferred', questionId, reason: 'delivery-unavailable' });
|
|
1301
|
+
continue;
|
|
1302
|
+
}
|
|
1303
|
+
if (existing !== undefined) {
|
|
1304
|
+
if (typeof existing.outboxId !== 'string' || existing.outboxId.length === 0 || existing.outboxId.length > 256) {
|
|
1305
|
+
push({ action: 'deferred', questionId, reason: 'delivery-unavailable' });
|
|
1306
|
+
continue;
|
|
1307
|
+
}
|
|
1308
|
+
this.store.attentionQuestions.recordDelivery(scope, cohort, questionId, 'queued', existing.outboxId);
|
|
1309
|
+
push({ action: 'queued', questionId, outboxId: existing.outboxId });
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
if (!latest.open || this.store.attentionQuestions.reply(questionId) !== undefined) {
|
|
1313
|
+
this.store.attentionQuestions.recordDelivery(scope, cohort, questionId, 'closed', null);
|
|
1314
|
+
push({ action: 'closed', questionId });
|
|
1315
|
+
continue;
|
|
1316
|
+
}
|
|
1317
|
+
const expiresAt = question.createdAt + ttl;
|
|
1318
|
+
if (this.now() >= expiresAt) {
|
|
1319
|
+
this.store.attentionQuestions.recordDelivery(scope, cohort, questionId, 'expired', null);
|
|
1320
|
+
push({ action: 'expired', questionId });
|
|
1321
|
+
continue;
|
|
1322
|
+
}
|
|
1323
|
+
let principal;
|
|
1324
|
+
let sessionId;
|
|
1325
|
+
try {
|
|
1326
|
+
const receipt = delivery.validateOwnerRoute({ authorityId: owner.ownerRouteId, principalId: owner.principalId,
|
|
1327
|
+
workspace: scope.workspace, agentPreset: scope.preset });
|
|
1328
|
+
sessionId = delivery.resolveOwnerRoute(owner.ownerRouteId).binding.sessionId;
|
|
1329
|
+
principal = { principalRecordId: receipt.principalRecordId, principalVersion: receipt.principalVersion };
|
|
1330
|
+
}
|
|
1331
|
+
catch {
|
|
1332
|
+
push({ action: 'deferred', questionId, reason: 'route-mismatch' });
|
|
1333
|
+
continue;
|
|
1334
|
+
}
|
|
1335
|
+
let outboxId;
|
|
1336
|
+
try {
|
|
1337
|
+
outboxId = delivery.enqueueOwnerNotification({
|
|
1338
|
+
sourceId: ATTENTION_OWNER_QUESTION_SOURCE_ID,
|
|
1339
|
+
ownerRouteId: owner.ownerRouteId,
|
|
1340
|
+
scope: { principalId: owner.principalId, ...principal, workspace: scope.workspace, preset: scope.preset },
|
|
1341
|
+
sessionId,
|
|
1342
|
+
idempotencyKey,
|
|
1343
|
+
text: attentionOwnerQuestionText(question, scope, { repliesRead: true }),
|
|
1344
|
+
expiresAt,
|
|
1345
|
+
attention: { purpose: 'result' },
|
|
1346
|
+
withdrawable: true,
|
|
1347
|
+
withdrawalHandle,
|
|
1348
|
+
}).id;
|
|
1349
|
+
}
|
|
1350
|
+
catch {
|
|
1351
|
+
push({ action: 'deferred', questionId, reason: 'enqueue-failed' });
|
|
1352
|
+
continue;
|
|
1353
|
+
}
|
|
1354
|
+
if (typeof outboxId !== 'string' || outboxId.length === 0 || outboxId.length > 256) {
|
|
1355
|
+
push({ action: 'deferred', questionId, reason: 'enqueue-failed' });
|
|
1356
|
+
continue;
|
|
1357
|
+
}
|
|
1358
|
+
this.store.attentionQuestions.recordDelivery(scope, cohort, questionId, 'queued', outboxId);
|
|
1359
|
+
push({ action: 'queued', questionId, outboxId });
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
return Object.freeze(results);
|
|
1363
|
+
}
|
|
1364
|
+
/**
|
|
1365
|
+
* Host-internal record-only owner reply to the cohort's latest question.
|
|
1366
|
+
* It never records a grant or changes a tuning version, and it is not the
|
|
1367
|
+
* slice-5 apply path; an owner who authorizes uses
|
|
1368
|
+
* `applyAttentionOwnerQuestionReply` (the only path the Delivery reply
|
|
1369
|
+
* consumer calls) or records a host grant through `grantAttentionLoosening`.
|
|
1370
|
+
*/
|
|
1371
|
+
recordAttentionOwnerQuestionReply(input) {
|
|
1372
|
+
this.assertActive();
|
|
1373
|
+
const cohort = this.#attentionCohort(input.cohort);
|
|
1374
|
+
if (!this.config.attentionOwnerQuestionDeliveryEnabled)
|
|
1375
|
+
return Object.freeze({ status: 'rejected', reason: 'delivery-disabled' });
|
|
1376
|
+
const latest = this.store.attention.latestOwnerQuestion(input.scope, cohort);
|
|
1377
|
+
if (latest === undefined || latest === 'unverifiable' || latest.questionId !== input.questionId) {
|
|
1378
|
+
return Object.freeze({ status: 'rejected', reason: 'unknown-question' });
|
|
1379
|
+
}
|
|
1380
|
+
// A question whose notice expired (recorded, or past the TTL) no longer takes a reply.
|
|
1381
|
+
const expired = this.store.attentionQuestions.delivery(latest.questionId)?.status === 'expired'
|
|
1382
|
+
|| this.now() >= latest.question.createdAt + this.config.attentionOwnerQuestionNoticeTtlMs;
|
|
1383
|
+
return this.store.attentionQuestions.recordReply(input.scope, cohort, latest.questionId, input.choice, latest.open, expired);
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Slice 5 evidence-backed apply path for one exact owner reply; the
|
|
1387
|
+
* Delivery consumer and Host callers use this instead of the record-only
|
|
1388
|
+
* seam. `authorize-once` records the reply and creates one bounded one-time
|
|
1389
|
+
* grant for exactly the questioned proposal (parent/candidate bounds, the
|
|
1390
|
+
* proposal's step size, one adoption); the next tuning tick still re-runs
|
|
1391
|
+
* the independent holdout replay before adopting. `decline` closes the
|
|
1392
|
+
* proposal. A proposal that already had an immutable decision returns
|
|
1393
|
+
* `proposal-terminal` and nothing adopts.
|
|
1394
|
+
*/
|
|
1395
|
+
applyAttentionOwnerQuestionReply(input) {
|
|
1396
|
+
this.assertActive();
|
|
1397
|
+
const cohort = this.#attentionCohort(input.cohort);
|
|
1398
|
+
if (!this.config.attentionOwnerQuestionDeliveryEnabled)
|
|
1399
|
+
return Object.freeze({ status: 'rejected', reason: 'delivery-disabled' });
|
|
1400
|
+
return this.store.attention.applyOwnerQuestionReply(input.scope, cohort, input.questionId, input.choice, { noticeTtlMs: this.config.attentionOwnerQuestionNoticeTtlMs });
|
|
1401
|
+
}
|
|
1402
|
+
/** Per-question delivery decisions and recorded replies for one cohort. */
|
|
1403
|
+
attentionOwnerQuestionStatus(input) {
|
|
1404
|
+
this.assertActive();
|
|
1405
|
+
return this.store.attentionQuestions.status(input.scope, this.#attentionCohort(input.cohort), this.config.attentionOwnerQuestionDeliveryEnabled);
|
|
1406
|
+
}
|
|
703
1407
|
limits() {
|
|
704
1408
|
this.assertActive();
|
|
705
1409
|
return Object.freeze({
|
|
@@ -717,6 +1421,23 @@ export class AssistantEvaluationService extends Service {
|
|
|
717
1421
|
return this.active && typeof registration === 'object' && registration !== null
|
|
718
1422
|
&& this.activeAutomationRegistrations.has(registration);
|
|
719
1423
|
}
|
|
1424
|
+
/**
|
|
1425
|
+
* Delivery's slot-anchored proof that an owner-notification withdrawal
|
|
1426
|
+
* registration under the `assistantEvaluation` authority is this live
|
|
1427
|
+
* service's own; a foreign object claiming that authority never passes.
|
|
1428
|
+
*/
|
|
1429
|
+
ownsOwnerNotificationWithdrawal(registration) {
|
|
1430
|
+
return this.active && typeof registration === 'object' && registration !== null
|
|
1431
|
+
&& this.activeOwnerQuestionWithdrawals.has(registration);
|
|
1432
|
+
}
|
|
1433
|
+
/**
|
|
1434
|
+
* Delivery's slot-anchored proof that the slice-5 reply consumer under the
|
|
1435
|
+
* `assistantEvaluation` authority is this live service's own registration.
|
|
1436
|
+
*/
|
|
1437
|
+
ownsOwnerNotificationReply(registration) {
|
|
1438
|
+
return this.active && typeof registration === 'object' && registration !== null
|
|
1439
|
+
&& this.activeOwnerQuestionReplies.has(registration);
|
|
1440
|
+
}
|
|
720
1441
|
/** Delivery uses the same ownership boundary as Automations. */
|
|
721
1442
|
ownsTrustedDeliveryEvaluationRegistration(registration) {
|
|
722
1443
|
return this.active && typeof registration === 'object' && registration !== null
|
|
@@ -726,6 +1447,11 @@ export class AssistantEvaluationService extends Service {
|
|
|
726
1447
|
return this.active && typeof registration === 'object' && registration !== null
|
|
727
1448
|
&& this.activeVerifierRegistrations.has(registration);
|
|
728
1449
|
}
|
|
1450
|
+
/** Work-items exports settled outcome revisions through the same ownership boundary. */
|
|
1451
|
+
ownsTrustedWorkItemEvaluationRegistration(registration) {
|
|
1452
|
+
return this.active && typeof registration === 'object' && registration !== null
|
|
1453
|
+
&& this.activeWorkItemRegistrations.has(registration);
|
|
1454
|
+
}
|
|
729
1455
|
review(agent, input = {}) {
|
|
730
1456
|
this.assertActive();
|
|
731
1457
|
const scope = this.agentScope(agent, 'evaluation_review');
|
|
@@ -871,7 +1597,22 @@ export class AssistantEvaluationService extends Service {
|
|
|
871
1597
|
if (current?.generation === generation)
|
|
872
1598
|
return current.dispose;
|
|
873
1599
|
const capabilities = new WeakMap();
|
|
1600
|
+
const workItemProofs = new WeakMap();
|
|
874
1601
|
let registered = true;
|
|
1602
|
+
// A WorkItems resolver is foreign code and can synchronously withdraw
|
|
1603
|
+
// this Delivery capability. Check generation first, then our own fields
|
|
1604
|
+
// after that last producer callback, directly before local mutation.
|
|
1605
|
+
const assertWorkItemDeliveryLocal = () => {
|
|
1606
|
+
this.assertActive();
|
|
1607
|
+
if (!registered || this.deliveryBinding?.producer !== producer || this.deliveryBinding.generation !== generation
|
|
1608
|
+
|| !this.activeDeliveryRegistrations.has(registration)) {
|
|
1609
|
+
throw new AssistantEvaluationError('forbidden', 'stale delivery Evaluation producer capability');
|
|
1610
|
+
}
|
|
1611
|
+
};
|
|
1612
|
+
const assertWorkItemDeliveryCurrent = () => {
|
|
1613
|
+
this.assertCurrentProducer(producer, generation, registered, 'delivery');
|
|
1614
|
+
assertWorkItemDeliveryLocal();
|
|
1615
|
+
};
|
|
875
1616
|
const registration = Object.freeze({
|
|
876
1617
|
protocol: TRUSTED_EVALUATION_PRODUCER_PROTOCOL,
|
|
877
1618
|
producer: 'assistant-delivery',
|
|
@@ -881,16 +1622,26 @@ export class AssistantEvaluationService extends Service {
|
|
|
881
1622
|
issueCapability: (claims) => {
|
|
882
1623
|
this.assertCurrentProducer(producer, generation, registered, 'delivery');
|
|
883
1624
|
const normalized = this.deliveryClaims(claims);
|
|
1625
|
+
let workItemProofDigest;
|
|
884
1626
|
if (normalized.subjectKind === 'goal-outcome') {
|
|
885
1627
|
const { proof, verifierOutcome } = this.resolveGoalOutcomeOwnerProof(normalized);
|
|
886
1628
|
if (this.store.adoptTrustedGoalOutcomeOwnerBaseline(normalized, proof, verifierOutcome)) {
|
|
887
1629
|
this.notifyTrustedTaskChange();
|
|
888
1630
|
}
|
|
889
1631
|
}
|
|
1632
|
+
else if (normalized.subjectKind === 'work-item') {
|
|
1633
|
+
if (typeof producer.withOwnerWorkItemFeedbackWriterFence !== 'function') {
|
|
1634
|
+
throw new AssistantEvaluationError('forbidden', 'Delivery work-item feedback writer fence is unavailable');
|
|
1635
|
+
}
|
|
1636
|
+
workItemProofDigest = this.resolveWorkItemOwnerProof(normalized).proofDigest;
|
|
1637
|
+
assertWorkItemDeliveryCurrent();
|
|
1638
|
+
}
|
|
890
1639
|
else
|
|
891
1640
|
this.store.adoptLegacyOwnerFeedback(normalized);
|
|
892
1641
|
const capability = Object.freeze(Object.create(null));
|
|
893
1642
|
capabilities.set(capability, normalized);
|
|
1643
|
+
if (workItemProofDigest !== undefined)
|
|
1644
|
+
workItemProofs.set(capability, workItemProofDigest);
|
|
894
1645
|
return capability;
|
|
895
1646
|
},
|
|
896
1647
|
inspect: (capabilityReceipt) => {
|
|
@@ -902,6 +1653,12 @@ export class AssistantEvaluationService extends Service {
|
|
|
902
1653
|
throw new AssistantEvaluationError('forbidden', 'missing owner lineage');
|
|
903
1654
|
if (claims.subjectKind === 'goal-outcome')
|
|
904
1655
|
this.resolveGoalOutcomeOwnerProof(claims);
|
|
1656
|
+
if (claims.subjectKind === 'work-item'
|
|
1657
|
+
&& this.resolveWorkItemOwnerProof(claims).proofDigest !== workItemProofs.get(capabilityReceipt)) {
|
|
1658
|
+
throw new AssistantEvaluationError('forbidden', 'work-item proof changed since owner capability issue');
|
|
1659
|
+
}
|
|
1660
|
+
if (claims.subjectKind === 'work-item')
|
|
1661
|
+
assertWorkItemDeliveryCurrent();
|
|
905
1662
|
return this.store.ownerObjectiveState(claims.scope, claims.subjectKind ?? 'automation-run', claims.subjectRef ?? claims.runId, claims.ownerCommand.principalRecordId, claims.ownerCommand.principalVersion);
|
|
906
1663
|
},
|
|
907
1664
|
append: (input) => {
|
|
@@ -924,6 +1681,59 @@ export class AssistantEvaluationService extends Service {
|
|
|
924
1681
|
}
|
|
925
1682
|
if (claims.subjectKind === 'goal-outcome')
|
|
926
1683
|
this.resolveGoalOutcomeOwnerProof(claims);
|
|
1684
|
+
if (claims.subjectKind === 'work-item') {
|
|
1685
|
+
const proof = this.resolveWorkItemOwnerProof(claims);
|
|
1686
|
+
assertWorkItemDeliveryCurrent();
|
|
1687
|
+
if (proof.proofDigest !== workItemProofs.get(input.capabilityReceipt))
|
|
1688
|
+
throw new AssistantEvaluationError('forbidden', 'work-item proof changed since owner capability issue');
|
|
1689
|
+
// Production termination is its own trusted fact. It is persisted
|
|
1690
|
+
// with objective unknown before the separately authenticated owner vote.
|
|
1691
|
+
this.store.append({
|
|
1692
|
+
scope: claims.scope, situation: claims.situation,
|
|
1693
|
+
executionStatus: proof.production.executionStatus, objectiveStatus: 'unknown', deliveryStatus: 'unknown',
|
|
1694
|
+
source: { kind: 'system', id: 'assistant-work-items-owner-proof' }, trust: 'trusted',
|
|
1695
|
+
evidence: [
|
|
1696
|
+
{ kind: 'work-item-owner-result', ref: proof.itemId },
|
|
1697
|
+
{ kind: 'work-item-report', ref: `${proof.itemId}#${String(proof.reportRevision)}`, digest: proof.reportDigest },
|
|
1698
|
+
{ kind: 'work-item-production-proof', ref: proof.production.runId, digest: proof.proofDigest },
|
|
1699
|
+
], metrics: {}, occurredAt: proof.production.completedAt,
|
|
1700
|
+
idempotencyKey: `work-item-owner-proof:${acceptanceDigest([proof.itemId, proof.proofDigest])}`,
|
|
1701
|
+
evaluator: { id: 'assistant-work-items-owner-proof', version: '1' },
|
|
1702
|
+
});
|
|
1703
|
+
// The native producer can run callbacks. Re-prove it after durable
|
|
1704
|
+
// baseline creation and directly before the owner mutation.
|
|
1705
|
+
const finalProof = this.resolveWorkItemOwnerProof(claims);
|
|
1706
|
+
assertWorkItemDeliveryCurrent();
|
|
1707
|
+
if (acceptanceDigest(finalProof) !== acceptanceDigest(proof))
|
|
1708
|
+
throw new AssistantEvaluationError('forbidden', 'work-item proof changed before owner append');
|
|
1709
|
+
if (typeof producer.withOwnerWorkItemFeedbackWriterFence !== 'function') {
|
|
1710
|
+
throw new AssistantEvaluationError('forbidden', 'Delivery work-item feedback writer fence is unavailable');
|
|
1711
|
+
}
|
|
1712
|
+
const outcome = producer.withOwnerWorkItemFeedbackWriterFence(registration, claims, finalProof.proofDigest, () => {
|
|
1713
|
+
// This callback runs while Delivery owns its source writer lock.
|
|
1714
|
+
// No producer calls or notifications may re-enter after its final
|
|
1715
|
+
// owner/source/Policy check and before this local owner mutation.
|
|
1716
|
+
assertWorkItemDeliveryLocal();
|
|
1717
|
+
return this.store.append({
|
|
1718
|
+
scope: claims.scope, situation: claims.situation,
|
|
1719
|
+
executionStatus: proof.production.executionStatus, objectiveStatus: claims.objectiveStatus,
|
|
1720
|
+
deliveryStatus: 'delivered', source: { kind: 'user-feedback', id: 'assistant-delivery/typed-owner-feedback' }, trust: 'trusted',
|
|
1721
|
+
evidence: [
|
|
1722
|
+
{ kind: 'work-item-owner-result', ref: proof.itemId },
|
|
1723
|
+
{ kind: 'work-item-report', ref: `${proof.itemId}#${String(proof.reportRevision)}`, digest: proof.reportDigest },
|
|
1724
|
+
{ kind: 'delivery-outbox', ref: claims.outboxId },
|
|
1725
|
+
{ kind: 'work-item-production-proof', ref: proof.production.runId, digest: proof.proofDigest },
|
|
1726
|
+
], metrics: {}, occurredAt: claims.occurredAt, idempotencyKey: claims.idempotencyKey,
|
|
1727
|
+
evaluator: { id: 'assistant-delivery-owner-feedback', version: '2' },
|
|
1728
|
+
}, claims.ownerCommand);
|
|
1729
|
+
});
|
|
1730
|
+
// Delivery has now committed its recorded source acknowledgement.
|
|
1731
|
+
// A lost cross-database acknowledgement remains a durable unknown;
|
|
1732
|
+
// it must recover the same operation instead of replaying a new vote.
|
|
1733
|
+
assertWorkItemDeliveryLocal();
|
|
1734
|
+
this.notifyTrustedTaskChange();
|
|
1735
|
+
return outcome;
|
|
1736
|
+
}
|
|
927
1737
|
return this.appendTrusted({
|
|
928
1738
|
scope: claims.scope,
|
|
929
1739
|
situation: claims.situation,
|
|
@@ -1086,6 +1896,114 @@ export class AssistantEvaluationService extends Service {
|
|
|
1086
1896
|
previous?.dispose();
|
|
1087
1897
|
return dispose;
|
|
1088
1898
|
}
|
|
1899
|
+
/**
|
|
1900
|
+
* Bind the settled work-item producer.
|
|
1901
|
+
*
|
|
1902
|
+
* The envelope's objective and delivery status are always `unknown` and its
|
|
1903
|
+
* execution status is derived here from the evidence state, never read from
|
|
1904
|
+
* the producer. A work item can therefore contribute durable evidence without
|
|
1905
|
+
* ever upgrading its own objective claim through Evaluation.
|
|
1906
|
+
*/
|
|
1907
|
+
bindWorkItemsProducer(producer) {
|
|
1908
|
+
const generation = hostIdentifier(producer.trustedWorkItemProducerGeneration(), 'trusted work-item generation', 200);
|
|
1909
|
+
const current = this.workItemsBinding;
|
|
1910
|
+
if (current?.generation === generation)
|
|
1911
|
+
return current.dispose;
|
|
1912
|
+
let registered = true;
|
|
1913
|
+
let disposeRegistration;
|
|
1914
|
+
let binding;
|
|
1915
|
+
const assertCurrent = () => {
|
|
1916
|
+
this.assertActive();
|
|
1917
|
+
if (!registered || this.workItemsBinding?.producer !== producer
|
|
1918
|
+
|| this.workItemsBinding.generation !== generation
|
|
1919
|
+
|| producer.trustedWorkItemProducerGeneration() !== generation) {
|
|
1920
|
+
throw new AssistantEvaluationError('forbidden', 'stale work-item Evaluation producer capability');
|
|
1921
|
+
}
|
|
1922
|
+
};
|
|
1923
|
+
const registration = Object.freeze({
|
|
1924
|
+
protocol: 'assistant-work-items/evaluation/v1',
|
|
1925
|
+
generation,
|
|
1926
|
+
owner: this,
|
|
1927
|
+
append: async (claims) => {
|
|
1928
|
+
assertCurrent();
|
|
1929
|
+
if (typeof claims !== 'object' || claims === null || Array.isArray(claims)) {
|
|
1930
|
+
throw new AssistantEvaluationError('forbidden', 'work-item Evaluation claims are invalid');
|
|
1931
|
+
}
|
|
1932
|
+
const itemId = hostIdentifier(claims.itemId, 'work item id', 200);
|
|
1933
|
+
const revision = claims.outcomeRevision;
|
|
1934
|
+
const settledBy = claims.settledBy;
|
|
1935
|
+
const evidenceState = claims.evidenceState;
|
|
1936
|
+
const occurredAt = claims.occurredAt;
|
|
1937
|
+
if (!Number.isSafeInteger(revision) || revision < 1
|
|
1938
|
+
|| !['report-ready', 'report-blocked', 'report-failed', 'withdrawn', 'retry-exhausted', 'observed-evidence', 'window-expired', 'closed-by-thread'].includes(settledBy)
|
|
1939
|
+
|| !['observed', 'unverified', 'not-applicable'].includes(evidenceState)) {
|
|
1940
|
+
throw new AssistantEvaluationError('forbidden', 'work-item Evaluation settlement is invalid');
|
|
1941
|
+
}
|
|
1942
|
+
// The registration type already limits these to the literal `'unknown'`;
|
|
1943
|
+
// re-proving them at runtime keeps a forged capability from widening it.
|
|
1944
|
+
if (claims.objectiveStatus !== 'unknown' || claims.deliveryStatus !== 'unknown') {
|
|
1945
|
+
throw new AssistantEvaluationError('forbidden', 'work-item Evaluation claims cannot assert an objective');
|
|
1946
|
+
}
|
|
1947
|
+
const idempotencyKey = `work-items/${itemId}/${String(revision)}/${evidenceState}`;
|
|
1948
|
+
if (claims.idempotencyKey !== idempotencyKey) {
|
|
1949
|
+
throw new AssistantEvaluationError('forbidden', 'work-item Evaluation identity is not deterministic');
|
|
1950
|
+
}
|
|
1951
|
+
const digest = hostIdentifier(claims.digest, 'work-item outcome digest', 200);
|
|
1952
|
+
const situation = hostIdentifier(claims.situation, 'work-item situation', this.config.maxSituationBytes);
|
|
1953
|
+
if (!Number.isSafeInteger(occurredAt) || occurredAt < 0 || occurredAt > this.now()) {
|
|
1954
|
+
throw new AssistantEvaluationError('forbidden', 'work-item Evaluation occurrence is invalid');
|
|
1955
|
+
}
|
|
1956
|
+
const scope = canonicalEvaluationScope(claims.scope).scope;
|
|
1957
|
+
this.appendTrusted({
|
|
1958
|
+
scope,
|
|
1959
|
+
situation,
|
|
1960
|
+
executionStatus: evidenceState === 'observed' ? 'succeeded' : 'unknown',
|
|
1961
|
+
objectiveStatus: 'unknown',
|
|
1962
|
+
deliveryStatus: 'unknown',
|
|
1963
|
+
source: { kind: 'system', id: 'assistant-work-items' },
|
|
1964
|
+
trust: 'trusted',
|
|
1965
|
+
evidence: [
|
|
1966
|
+
{ kind: 'work-item', ref: itemId },
|
|
1967
|
+
{ kind: 'work-item-outcome', ref: `${itemId}#${String(revision)}`, digest },
|
|
1968
|
+
],
|
|
1969
|
+
metrics: claims.metrics,
|
|
1970
|
+
occurredAt,
|
|
1971
|
+
idempotencyKey,
|
|
1972
|
+
evaluator: {
|
|
1973
|
+
id: 'assistant-work-items',
|
|
1974
|
+
version: evidenceState === 'observed' ? 'settlement-v1' : 'unverified-v1',
|
|
1975
|
+
},
|
|
1976
|
+
});
|
|
1977
|
+
},
|
|
1978
|
+
});
|
|
1979
|
+
this.activeWorkItemRegistrations.add(registration);
|
|
1980
|
+
const dispose = () => {
|
|
1981
|
+
if (!registered)
|
|
1982
|
+
return;
|
|
1983
|
+
registered = false;
|
|
1984
|
+
this.activeWorkItemRegistrations.delete(registration);
|
|
1985
|
+
if (this.workItemsBinding === binding)
|
|
1986
|
+
this.workItemsBinding = undefined;
|
|
1987
|
+
disposeRegistration?.();
|
|
1988
|
+
};
|
|
1989
|
+
binding = Object.freeze({ producer, generation, dispose });
|
|
1990
|
+
const previous = this.workItemsBinding;
|
|
1991
|
+
this.workItemsBinding = binding;
|
|
1992
|
+
try {
|
|
1993
|
+
disposeRegistration = producer.registerTrustedWorkItemEvaluationSink(registration);
|
|
1994
|
+
if (typeof disposeRegistration !== 'function')
|
|
1995
|
+
throw new AssistantEvaluationError('forbidden', 'work-items returned no registration disposer');
|
|
1996
|
+
}
|
|
1997
|
+
catch (error) {
|
|
1998
|
+
registered = false;
|
|
1999
|
+
this.activeWorkItemRegistrations.delete(registration);
|
|
2000
|
+
if (this.workItemsBinding === binding)
|
|
2001
|
+
this.workItemsBinding = previous;
|
|
2002
|
+
throw error;
|
|
2003
|
+
}
|
|
2004
|
+
previous?.dispose();
|
|
2005
|
+
return dispose;
|
|
2006
|
+
}
|
|
1089
2007
|
producerGeneration(producer) {
|
|
1090
2008
|
return hostIdentifier(producer.trustedEvaluationProducerGeneration(), 'trusted Evaluation producer generation', 200);
|
|
1091
2009
|
}
|
|
@@ -1146,7 +2064,7 @@ export class AssistantEvaluationService extends Service {
|
|
|
1146
2064
|
const subjectKind = input.subjectKind ?? 'automation-run';
|
|
1147
2065
|
const subjectRef = input.subjectRef ?? input.runId;
|
|
1148
2066
|
if ((input.subjectKind !== undefined && input.subjectKind !== 'automation-run'
|
|
1149
|
-
&& input.subjectKind !== 'foreground-turn' && input.subjectKind !== 'goal-outcome')
|
|
2067
|
+
&& input.subjectKind !== 'foreground-turn' && input.subjectKind !== 'goal-outcome' && input.subjectKind !== 'work-item')
|
|
1150
2068
|
|| (input.subjectRef !== undefined && input.subjectRef.trim() === '')) {
|
|
1151
2069
|
throw new AssistantEvaluationError('forbidden', 'delivery Evaluation subject is invalid');
|
|
1152
2070
|
}
|
|
@@ -1164,7 +2082,16 @@ export class AssistantEvaluationService extends Service {
|
|
|
1164
2082
|
|| typeof input.goalOutcomeCapability !== 'object' || input.goalOutcomeCapability === null)) {
|
|
1165
2083
|
throw new AssistantEvaluationError('forbidden', 'goal outcome owner capability is invalid');
|
|
1166
2084
|
}
|
|
2085
|
+
if (subjectKind === 'work-item' && (input.subjectRef === undefined || command === undefined
|
|
2086
|
+
|| typeof input.workItemOutcomeCapability !== 'object' || input.workItemOutcomeCapability === null)) {
|
|
2087
|
+
throw new AssistantEvaluationError('forbidden', 'work-item owner capability is invalid');
|
|
2088
|
+
}
|
|
2089
|
+
if ((subjectKind !== 'work-item' && Object.hasOwn(input, 'workItemOutcomeCapability'))
|
|
2090
|
+
|| (subjectKind !== 'goal-outcome' && Object.hasOwn(input, 'goalOutcomeCapability'))) {
|
|
2091
|
+
throw new AssistantEvaluationError('forbidden', 'owner capability does not match subject');
|
|
2092
|
+
}
|
|
1167
2093
|
return Object.freeze({
|
|
2094
|
+
...(subjectKind === 'work-item' ? { workItemOutcomeCapability: input.workItemOutcomeCapability } : {}),
|
|
1168
2095
|
...(subjectKind === 'goal-outcome'
|
|
1169
2096
|
? { goalOutcomeCapability: input.goalOutcomeCapability } : {}),
|
|
1170
2097
|
...(input.initialIdempotencyKey === undefined ? {} : { initialIdempotencyKey: hostIdentifier(input.initialIdempotencyKey, 'initial feedback key', 200) }),
|
|
@@ -1186,6 +2113,39 @@ export class AssistantEvaluationService extends Service {
|
|
|
1186
2113
|
idempotencyKey: hostIdentifier(input.idempotencyKey, 'idempotencyKey', 200),
|
|
1187
2114
|
});
|
|
1188
2115
|
}
|
|
2116
|
+
resolveWorkItemOwnerProof(claims) {
|
|
2117
|
+
this.assertActive();
|
|
2118
|
+
const producer = this.ctx.get('assistantWorkItems', false);
|
|
2119
|
+
const binding = this.workItemsBinding;
|
|
2120
|
+
if (claims.subjectKind !== 'work-item' || !isTrustedWorkItemEvaluationProducer(producer)
|
|
2121
|
+
|| !binding || serviceIdentity(producer) !== serviceIdentity(binding.producer)
|
|
2122
|
+
|| producer.trustedWorkItemProducerGeneration() !== binding.generation
|
|
2123
|
+
|| typeof producer.resolveOwnerWorkItemFeedbackTarget !== 'function') {
|
|
2124
|
+
throw new AssistantEvaluationError('forbidden', 'current WorkItems owner capability resolver is unavailable');
|
|
2125
|
+
}
|
|
2126
|
+
let proof;
|
|
2127
|
+
try {
|
|
2128
|
+
const value = producer.resolveOwnerWorkItemFeedbackTarget(claims.workItemOutcomeCapability);
|
|
2129
|
+
proof = validateWorkItemOwnerFeedbackProof(value, this.now());
|
|
2130
|
+
}
|
|
2131
|
+
catch {
|
|
2132
|
+
throw new AssistantEvaluationError('forbidden', 'WorkItems owner capability is stale or invalid');
|
|
2133
|
+
}
|
|
2134
|
+
const currentGeneration = producer.trustedWorkItemProducerGeneration();
|
|
2135
|
+
const current = this.ctx.get('assistantWorkItems', false);
|
|
2136
|
+
this.assertActive();
|
|
2137
|
+
if (currentGeneration !== binding.generation || !current || typeof current !== 'object' || this.workItemsBinding !== binding
|
|
2138
|
+
|| serviceIdentity(current) !== serviceIdentity(producer)
|
|
2139
|
+
|| claims.subjectRef !== proof.itemId || claims.runId !== proof.production.runId
|
|
2140
|
+
|| claims.scope.workspace !== proof.scope.workspace || claims.scope.preset !== proof.scope.preset
|
|
2141
|
+
|| claims.situation !== `work-item:${proof.itemKind}` || claims.principalId !== proof.owner.principalId
|
|
2142
|
+
|| !claims.ownerCommand || claims.ownerCommand.principalRecordId !== proof.owner.principalRecordId
|
|
2143
|
+
|| claims.ownerCommand.principalVersion !== proof.owner.principalVersion
|
|
2144
|
+
|| claims.occurredAt < proof.report.recordedAt || claims.occurredAt > this.now()) {
|
|
2145
|
+
throw new AssistantEvaluationError('forbidden', 'WorkItems proof does not match current Delivery claims');
|
|
2146
|
+
}
|
|
2147
|
+
return proof;
|
|
2148
|
+
}
|
|
1189
2149
|
resolveGoalOutcomeOwnerProof(claims) {
|
|
1190
2150
|
const goals = this.ctx.get('assistantGoals', false);
|
|
1191
2151
|
if (!isTrustedGoalsOutcomeOwnerResolver(goals)) {
|