@dsh-enhanced/assistant-evaluation 0.1.64 → 0.1.66
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 +54 -1
- package/lib/attention-owner-questions.d.ts +85 -0
- package/lib/attention-owner-questions.d.ts.map +1 -0
- package/lib/attention-owner-questions.js +142 -0
- package/lib/attention-owner-questions.js.map +1 -0
- package/lib/attention-tuning.d.ts +431 -0
- package/lib/attention-tuning.d.ts.map +1 -0
- package/lib/attention-tuning.js +892 -0
- package/lib/attention-tuning.js.map +1 -0
- package/lib/benchmark/cli.d.ts.map +1 -1
- package/lib/benchmark/cli.js +11 -6
- 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.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/types.d.ts +64 -7
- package/lib/benchmark/types.d.ts.map +1 -1
- package/lib/index.d.ts +7 -0
- 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 +244 -2
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +830 -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 +120 -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 +30 -30
package/lib/service.js
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
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, } 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
|
+
}
|
|
7
18
|
export const ASSISTANT_EVALUATION_SKILL = `# Personal assistant self-evaluation
|
|
8
19
|
|
|
9
20
|
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 +45,31 @@ const configSchema = Schema.object({
|
|
|
34
45
|
projectionTimeoutMs: Schema.number().step(1).min(100).max(300_000).default(2_000),
|
|
35
46
|
projectionRetryBaseMs: Schema.number().step(1).min(1).max(3_600_000).default(1_000),
|
|
36
47
|
projectionRetryMaxMs: Schema.number().step(1).min(1).max(86_400_000).default(3_600_000),
|
|
48
|
+
notificationOutcomesEnabled: Schema.boolean().default(false),
|
|
49
|
+
notificationResponseWindowMs: Schema.number().step(1).min(60_000).max(2_592_000_000).default(86_400_000),
|
|
50
|
+
notificationObservationHorizonMs: Schema.number().step(1).min(60_000).max(31_536_000_000).default(2_592_000_000),
|
|
51
|
+
notificationSyncBatchSize: Schema.number().step(1).min(1).max(500).default(100),
|
|
52
|
+
notificationSyncIntervalMs: Schema.number().step(1).min(0).max(86_400_000).default(60_000),
|
|
53
|
+
attentionTuningAutoAdopt: Schema.boolean().default(false),
|
|
54
|
+
intakeAttentionAgreementEnabled: Schema.boolean().default(false),
|
|
55
|
+
attentionTuningCohorts: Schema.array(Schema.object({
|
|
56
|
+
lane: Schema.string().required(),
|
|
57
|
+
sourceId: Schema.string().required(),
|
|
58
|
+
})).default([{ lane: 'owner-notification', sourceId: 'assistant-proactive/v1' }]),
|
|
59
|
+
attentionTuningMinSample: Schema.number().step(1).min(5).max(10_000).default(20),
|
|
60
|
+
attentionTuningNegativeRatePpm: Schema.number().step(1).min(100_000).max(1_000_000).default(600_000),
|
|
61
|
+
attentionTuningMuteRatePpm: Schema.number().step(1).min(100_000).max(1_000_000).default(900_000),
|
|
62
|
+
attentionTuningRegressionMarginPpm: Schema.number().step(1).min(0).max(1_000_000).default(100_000),
|
|
63
|
+
attentionTuningObservationMs: Schema.number().step(1).min(60_000).max(31_536_000_000).default(604_800_000),
|
|
64
|
+
attentionTuningEvaluationWindowMs: Schema.number().step(1).min(60_000).max(31_536_000_000).default(604_800_000),
|
|
65
|
+
attentionLooseningEnabled: Schema.boolean().default(false),
|
|
66
|
+
attentionLooseningReplayWindowMs: Schema.number().step(1).min(3_600_000).max(31_536_000_000).default(2_592_000_000),
|
|
67
|
+
attentionOwnerQuestionDeliveryEnabled: Schema.boolean().default(false),
|
|
68
|
+
attentionOwnerQuestionOwner: Schema.union([Schema.object({
|
|
69
|
+
ownerRouteId: Schema.string().required(),
|
|
70
|
+
principalId: Schema.string().required(),
|
|
71
|
+
})]),
|
|
72
|
+
attentionOwnerQuestionNoticeTtlMs: Schema.number().step(1).min(60_000).max(2_592_000_000).default(86_400_000),
|
|
37
73
|
});
|
|
38
74
|
export class AssistantEvaluationError extends Error {
|
|
39
75
|
code;
|
|
@@ -59,6 +95,10 @@ export function canonicalEvaluationHostScope(input) {
|
|
|
59
95
|
});
|
|
60
96
|
return Object.freeze(branded);
|
|
61
97
|
}
|
|
98
|
+
function isNotificationOutcomeFactSource(value) {
|
|
99
|
+
return typeof value === 'object' && value !== null
|
|
100
|
+
&& typeof value.inspectNotificationOutcomeFacts === 'function';
|
|
101
|
+
}
|
|
62
102
|
function exactEvaluationHostScope(input) {
|
|
63
103
|
if (typeof input !== 'object' || input === null || !Object.isFrozen(input)
|
|
64
104
|
|| input[evaluationHostScopeBrand] !== true) {
|
|
@@ -173,6 +213,11 @@ function isTrustedVerifierEvaluationProducer(value) {
|
|
|
173
213
|
&& typeof value.trustedVerificationProducerGeneration === 'function'
|
|
174
214
|
&& typeof value.registerTrustedVerifierEvaluationSink === 'function';
|
|
175
215
|
}
|
|
216
|
+
function isTrustedWorkItemEvaluationProducer(value) {
|
|
217
|
+
return typeof value === 'object' && value !== null
|
|
218
|
+
&& typeof value.trustedWorkItemProducerGeneration === 'function'
|
|
219
|
+
&& typeof value.registerTrustedWorkItemEvaluationSink === 'function';
|
|
220
|
+
}
|
|
176
221
|
function isTrustedEvaluationProjector(value) {
|
|
177
222
|
return typeof value === 'object' && value !== null
|
|
178
223
|
&& typeof value.projectTrustedEvaluationTaskRevision === 'function';
|
|
@@ -194,11 +239,18 @@ export class AssistantEvaluationService extends Service {
|
|
|
194
239
|
automationBinding;
|
|
195
240
|
deliveryBinding;
|
|
196
241
|
verifierBinding;
|
|
242
|
+
workItemsBinding;
|
|
197
243
|
activeAutomationRegistrations = new WeakSet();
|
|
198
244
|
activeDeliveryRegistrations = new WeakSet();
|
|
199
245
|
activeVerifierRegistrations = new WeakSet();
|
|
246
|
+
activeWorkItemRegistrations = new WeakSet();
|
|
200
247
|
activeProjections = new Map();
|
|
201
248
|
projectionTimer;
|
|
249
|
+
notificationFacts;
|
|
250
|
+
lastNotificationSyncAt = Number.NEGATIVE_INFINITY;
|
|
251
|
+
ownerQuestionWithdrawal;
|
|
252
|
+
activeOwnerQuestionWithdrawals = new WeakSet();
|
|
253
|
+
attentionCohorts;
|
|
202
254
|
active = true;
|
|
203
255
|
constructor(ctx, input, options = {}) {
|
|
204
256
|
super(ctx, 'assistantEvaluation');
|
|
@@ -217,6 +269,30 @@ export class AssistantEvaluationService extends Service {
|
|
|
217
269
|
if (this.config.projectionRetryBaseMs > this.config.projectionRetryMaxMs) {
|
|
218
270
|
throw new Error('assistant-evaluation: projectionRetryBaseMs must not exceed projectionRetryMaxMs');
|
|
219
271
|
}
|
|
272
|
+
if (this.config.intakeAttentionAgreementEnabled && !this.config.notificationOutcomesEnabled) {
|
|
273
|
+
throw new Error('assistant-evaluation: intakeAttentionAgreementEnabled requires notificationOutcomesEnabled');
|
|
274
|
+
}
|
|
275
|
+
if (this.config.attentionTuningAutoAdopt && !this.config.notificationOutcomesEnabled) {
|
|
276
|
+
throw new Error('assistant-evaluation: attentionTuningAutoAdopt requires notificationOutcomesEnabled');
|
|
277
|
+
}
|
|
278
|
+
if (this.config.attentionLooseningEnabled && !this.config.attentionTuningAutoAdopt) {
|
|
279
|
+
throw new Error('assistant-evaluation: attentionLooseningEnabled requires attentionTuningAutoAdopt');
|
|
280
|
+
}
|
|
281
|
+
if (this.config.attentionOwnerQuestionDeliveryEnabled && !this.config.attentionLooseningEnabled) {
|
|
282
|
+
throw new Error('assistant-evaluation: attentionOwnerQuestionDeliveryEnabled requires attentionLooseningEnabled');
|
|
283
|
+
}
|
|
284
|
+
if (this.config.attentionOwnerQuestionDeliveryEnabled && (typeof this.config.attentionOwnerQuestionOwner?.ownerRouteId !== 'string'
|
|
285
|
+
|| this.config.attentionOwnerQuestionOwner.ownerRouteId.length === 0 || typeof this.config.attentionOwnerQuestionOwner.principalId !== 'string'
|
|
286
|
+
|| this.config.attentionOwnerQuestionOwner.principalId.length === 0)) {
|
|
287
|
+
throw new Error('assistant-evaluation: attentionOwnerQuestionDeliveryEnabled requires attentionOwnerQuestionOwner');
|
|
288
|
+
}
|
|
289
|
+
if (this.config.attentionTuningMuteRatePpm < this.config.attentionTuningNegativeRatePpm) {
|
|
290
|
+
throw new Error('assistant-evaluation: attentionTuningMuteRatePpm must not be below attentionTuningNegativeRatePpm');
|
|
291
|
+
}
|
|
292
|
+
if (this.config.attentionTuningCohorts.length > 16) {
|
|
293
|
+
throw new Error('assistant-evaluation: at most 16 attention tuning cohorts');
|
|
294
|
+
}
|
|
295
|
+
this.attentionCohorts = Object.freeze(this.config.attentionTuningCohorts.map(cohort => normalizeAttentionCohort(cohort)));
|
|
220
296
|
this.now = options.now ?? Date.now;
|
|
221
297
|
this.store = new EvaluationStore({
|
|
222
298
|
path: this.config.databasePath,
|
|
@@ -313,6 +389,19 @@ export class AssistantEvaluationService extends Service {
|
|
|
313
389
|
return;
|
|
314
390
|
return this.bindDeliveryProducer(producer);
|
|
315
391
|
});
|
|
392
|
+
const bindNotificationFacts = (source) => {
|
|
393
|
+
if (!isNotificationOutcomeFactSource(source))
|
|
394
|
+
return;
|
|
395
|
+
this.notificationFacts = source;
|
|
396
|
+
return () => { if (this.notificationFacts === source)
|
|
397
|
+
this.notificationFacts = undefined; };
|
|
398
|
+
};
|
|
399
|
+
bindNotificationFacts(currentDelivery);
|
|
400
|
+
ctx.inject(['assistantDelivery'], deliveryCtx => bindNotificationFacts(deliveryCtx.get('assistantDelivery')));
|
|
401
|
+
if (this.config.attentionOwnerQuestionDeliveryEnabled) {
|
|
402
|
+
this.bindOwnerQuestionWithdrawal(currentDelivery);
|
|
403
|
+
ctx.inject(['assistantDelivery'], deliveryCtx => this.bindOwnerQuestionWithdrawal(deliveryCtx.get('assistantDelivery')));
|
|
404
|
+
}
|
|
316
405
|
const currentVerifier = ctx.get('assistantVerifier');
|
|
317
406
|
if (isTrustedVerifierEvaluationProducer(currentVerifier))
|
|
318
407
|
this.bindVerifierProducer(currentVerifier);
|
|
@@ -322,8 +411,33 @@ export class AssistantEvaluationService extends Service {
|
|
|
322
411
|
return;
|
|
323
412
|
return this.bindVerifierProducer(producer);
|
|
324
413
|
});
|
|
414
|
+
const currentWorkItems = ctx.get('assistantWorkItems');
|
|
415
|
+
if (isTrustedWorkItemEvaluationProducer(currentWorkItems))
|
|
416
|
+
this.bindWorkItemsProducer(currentWorkItems);
|
|
417
|
+
ctx.inject(['assistantWorkItems'], workItemsCtx => {
|
|
418
|
+
const producer = workItemsCtx.get('assistantWorkItems');
|
|
419
|
+
if (!isTrustedWorkItemEvaluationProducer(producer))
|
|
420
|
+
return;
|
|
421
|
+
return this.bindWorkItemsProducer(producer);
|
|
422
|
+
});
|
|
325
423
|
if (this.config.projectionIntervalMs > 0) {
|
|
326
424
|
this.projectionTimer = setInterval(() => {
|
|
425
|
+
const now = this.now();
|
|
426
|
+
if (this.config.notificationOutcomesEnabled && now - this.lastNotificationSyncAt >= this.config.notificationSyncIntervalMs) {
|
|
427
|
+
this.lastNotificationSyncAt = now;
|
|
428
|
+
try {
|
|
429
|
+
this.reconcileNotificationOutcomes();
|
|
430
|
+
}
|
|
431
|
+
catch { /* isolated from projection; retried on a later tick */ }
|
|
432
|
+
try {
|
|
433
|
+
this.reconcileAttentionTuning();
|
|
434
|
+
}
|
|
435
|
+
catch { /* retried on a later sync */ }
|
|
436
|
+
try {
|
|
437
|
+
this.reconcileAttentionOwnerQuestions();
|
|
438
|
+
}
|
|
439
|
+
catch { /* retried on a later sync */ }
|
|
440
|
+
}
|
|
327
441
|
void this.reconcileProjections().catch(() => { });
|
|
328
442
|
}, this.config.projectionIntervalMs);
|
|
329
443
|
this.projectionTimer.unref?.();
|
|
@@ -333,10 +447,13 @@ export class AssistantEvaluationService extends Service {
|
|
|
333
447
|
if (this.projectionTimer !== undefined)
|
|
334
448
|
clearInterval(this.projectionTimer);
|
|
335
449
|
this.projectionTimer = undefined;
|
|
450
|
+
this.notificationFacts = undefined;
|
|
451
|
+
this.ownerQuestionWithdrawal?.dispose();
|
|
336
452
|
disposeCurrentProjector?.();
|
|
337
453
|
this.automationBinding?.dispose();
|
|
338
454
|
this.deliveryBinding?.dispose();
|
|
339
455
|
this.verifierBinding?.dispose();
|
|
456
|
+
this.workItemsBinding?.dispose();
|
|
340
457
|
toolsBinding?.dispose();
|
|
341
458
|
skillBinding?.dispose();
|
|
342
459
|
await Promise.allSettled(this.activeProjections.values());
|
|
@@ -412,8 +529,8 @@ export class AssistantEvaluationService extends Service {
|
|
|
412
529
|
if (state === undefined) {
|
|
413
530
|
throw new AssistantEvaluationError('not-found', 'exact Evaluation projection was not found');
|
|
414
531
|
}
|
|
415
|
-
if (state.status
|
|
416
|
-
return Object.freeze({ evaluationId, status:
|
|
532
|
+
if (state.status !== 'pending') {
|
|
533
|
+
return Object.freeze({ evaluationId, status: state.status, attemptCount: state.attemptCount });
|
|
417
534
|
}
|
|
418
535
|
const active = this.activeProjections.get(evaluationId);
|
|
419
536
|
if (active !== undefined)
|
|
@@ -434,7 +551,7 @@ export class AssistantEvaluationService extends Service {
|
|
|
434
551
|
}
|
|
435
552
|
return Object.freeze({
|
|
436
553
|
evaluationId,
|
|
437
|
-
status: state.status === '
|
|
554
|
+
status: state.status === 'pending' ? 'deferred' : state.status,
|
|
438
555
|
attemptCount: state.attemptCount,
|
|
439
556
|
});
|
|
440
557
|
}
|
|
@@ -513,6 +630,11 @@ export class AssistantEvaluationService extends Service {
|
|
|
513
630
|
this.assertActive();
|
|
514
631
|
return this.store.getForegroundLearningProjection(exactEvaluationHostScope(input.scope), hostIdentifier(input.inboxId, 'inboxId', 1_000));
|
|
515
632
|
}
|
|
633
|
+
/** Host-only exact current owner result for one durable work item. */
|
|
634
|
+
getTrustedWorkItemLearningProjection(input) {
|
|
635
|
+
this.assertActive();
|
|
636
|
+
return this.store.getWorkItemLearningProjection(exactEvaluationHostScope(input.scope), hostIdentifier(input.itemId, 'itemId', 1_000));
|
|
637
|
+
}
|
|
516
638
|
/** Host-only exact canonical whole-goal proof, addressed by assessment id. */
|
|
517
639
|
getTrustedGoalOutcomeLearningProjection(input) {
|
|
518
640
|
this.assertActive();
|
|
@@ -544,6 +666,7 @@ export class AssistantEvaluationService extends Service {
|
|
|
544
666
|
&& projection.subjectKind !== 'foreground-turn'
|
|
545
667
|
&& projection.subjectKind !== 'goal-step'
|
|
546
668
|
&& projection.subjectKind !== 'goal-outcome'
|
|
669
|
+
&& projection.subjectKind !== 'work-item'
|
|
547
670
|
&& projection.subjectKind !== 'outcome')
|
|
548
671
|
|| !exactHostIdentifier(projection.subjectRef, 'projection.subjectRef', 1_000)
|
|
549
672
|
|| !Number.isSafeInteger(projection.version) || projection.version < 1
|
|
@@ -700,6 +823,460 @@ export class AssistantEvaluationService extends Service {
|
|
|
700
823
|
this.assertActive();
|
|
701
824
|
return this.store.health();
|
|
702
825
|
}
|
|
826
|
+
/**
|
|
827
|
+
* WP22 deterministic sync: ingest new Delivery audit rows after the durable
|
|
828
|
+
* cursor, then refresh a bounded batch of known notifications inside the
|
|
829
|
+
* observation horizon (least recently checked first) so late replies,
|
|
830
|
+
* corrections, withdrawals and expiry produce new revisions. No model call,
|
|
831
|
+
* no Evolution projection and no parameter change. Missing Delivery is a
|
|
832
|
+
* zero result, as is a disabled `notificationOutcomesEnabled`.
|
|
833
|
+
*/
|
|
834
|
+
reconcileNotificationOutcomes() {
|
|
835
|
+
this.assertActive();
|
|
836
|
+
const source = this.notificationFacts;
|
|
837
|
+
if (!this.config.notificationOutcomesEnabled || source === undefined)
|
|
838
|
+
return Object.freeze({ ingested: 0, refreshed: 0, revised: 0, invalid: 0 });
|
|
839
|
+
const ledger = this.store.notifications;
|
|
840
|
+
const limit = this.config.notificationSyncBatchSize;
|
|
841
|
+
const window = this.config.notificationResponseWindowMs;
|
|
842
|
+
let ingested = 0;
|
|
843
|
+
let refreshed = 0;
|
|
844
|
+
let revised = 0;
|
|
845
|
+
let invalid = 0;
|
|
846
|
+
const apply = (fact) => {
|
|
847
|
+
try {
|
|
848
|
+
if (ledger.apply(fact, window).revised)
|
|
849
|
+
revised += 1;
|
|
850
|
+
return true;
|
|
851
|
+
}
|
|
852
|
+
catch (error) {
|
|
853
|
+
// A malformed fact or a fact whose scope disagrees with its recorded
|
|
854
|
+
// head is counted and skipped so one bad row never stalls the cursor.
|
|
855
|
+
if (error instanceof EvaluationStoreError && (error.code === 'invalid-input' || error.code === 'version-conflict')) {
|
|
856
|
+
invalid += 1;
|
|
857
|
+
return false;
|
|
858
|
+
}
|
|
859
|
+
throw error;
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
const page = source.inspectNotificationOutcomeFacts({ afterSequence: ledger.syncCursor(), limit });
|
|
863
|
+
for (const fact of page.facts)
|
|
864
|
+
if (apply(fact))
|
|
865
|
+
ingested += 1;
|
|
866
|
+
if (page.nextAfterSequence !== null)
|
|
867
|
+
ledger.advanceSyncCursor(page.nextAfterSequence);
|
|
868
|
+
// The horizon is measured from notification time while expiry is measured
|
|
869
|
+
// from send time, so a horizon near the response window would stop
|
|
870
|
+
// re-checking a delivered notification before it could expire. Keep at
|
|
871
|
+
// least one extra window of slack for send latency.
|
|
872
|
+
const horizon = Math.max(this.config.notificationObservationHorizonMs, 2 * window);
|
|
873
|
+
const due = ledger.dueForRefresh(this.now() - horizon, limit);
|
|
874
|
+
if (due.length > 0) {
|
|
875
|
+
const facts = source.inspectNotificationOutcomeFacts({ outboxIds: due }).facts;
|
|
876
|
+
const applied = new Set();
|
|
877
|
+
for (const fact of facts) {
|
|
878
|
+
if (!apply(fact))
|
|
879
|
+
continue;
|
|
880
|
+
refreshed += 1;
|
|
881
|
+
applied.add(fact.outboxId);
|
|
882
|
+
}
|
|
883
|
+
// Missing or rejected facts still rotate to the back of the re-check
|
|
884
|
+
// order so they cannot starve the rest of the horizon.
|
|
885
|
+
for (const id of due)
|
|
886
|
+
if (!applied.has(id))
|
|
887
|
+
ledger.touch(id);
|
|
888
|
+
}
|
|
889
|
+
return Object.freeze({ ingested, refreshed, revised, invalid });
|
|
890
|
+
}
|
|
891
|
+
/** Current revision plus full revision history for one notification (Delivery Outbox id). */
|
|
892
|
+
inspectNotificationOutcome(input) {
|
|
893
|
+
this.assertActive();
|
|
894
|
+
return this.store.notifications.history(input.scope, hostIdentifier(input.notificationId, 'notificationId', 256));
|
|
895
|
+
}
|
|
896
|
+
/** Restart-safe feed of current notification heads after a per-scope notification watermark. */
|
|
897
|
+
notificationOutcomeFeed(input) {
|
|
898
|
+
this.assertActive();
|
|
899
|
+
return this.store.notifications.feed(input.scope, input.after, input.limit ?? 100);
|
|
900
|
+
}
|
|
901
|
+
/** Current-head counts by outcome and by attention verdict, over notification time. */
|
|
902
|
+
notificationOutcomeSummary(input) {
|
|
903
|
+
this.assertActive();
|
|
904
|
+
const to = input.toNotifiedAt ?? this.now() + 1;
|
|
905
|
+
const from = input.fromNotifiedAt ?? Math.max(0, to - this.config.defaultSummaryWindowMs);
|
|
906
|
+
if (!Number.isSafeInteger(from) || !Number.isSafeInteger(to) || from < 0 || to <= from
|
|
907
|
+
|| to - from > this.config.maxSummaryWindowMs) {
|
|
908
|
+
throw new AssistantEvaluationError('invalid-input', 'notification outcome summary window is invalid');
|
|
909
|
+
}
|
|
910
|
+
return this.store.notifications.summary(input.scope, from, to);
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* WP22, read-only and default off: agreement between the WP20 intake
|
|
914
|
+
* judgment behind each auto-admitted work item and the owner outcome of its
|
|
915
|
+
* notifications, over notification time. Undefined when
|
|
916
|
+
* `intakeAttentionAgreementEnabled` is off. Observe-mode intake creates no
|
|
917
|
+
* item and is therefore not measurable here. Observational (`causal:
|
|
918
|
+
* false`); it writes, adopts and sends nothing.
|
|
919
|
+
*/
|
|
920
|
+
inspectIntakeAttentionAgreement(input) {
|
|
921
|
+
this.assertActive();
|
|
922
|
+
if (!this.config.intakeAttentionAgreementEnabled)
|
|
923
|
+
return undefined;
|
|
924
|
+
const to = input.toNotifiedAt ?? this.now() + 1;
|
|
925
|
+
const from = input.fromNotifiedAt ?? Math.max(0, to - this.config.defaultSummaryWindowMs);
|
|
926
|
+
if (!Number.isSafeInteger(from) || !Number.isSafeInteger(to) || from < 0 || to <= from
|
|
927
|
+
|| to - from > this.config.maxSummaryWindowMs) {
|
|
928
|
+
throw new AssistantEvaluationError('invalid-input', 'intake attention agreement window is invalid');
|
|
929
|
+
}
|
|
930
|
+
const { scope } = canonicalEvaluationScope(input.scope);
|
|
931
|
+
const heads = this.store.notifications.workItemHeads(scope, from, to, INTAKE_ATTENTION_AGREEMENT_SCAN_LIMIT);
|
|
932
|
+
const workItems = this.ctx.get('assistantWorkItems');
|
|
933
|
+
return computeIntakeAttentionAgreement({ scope, fromNotifiedAt: from, toNotifiedAt: to, records: heads.records,
|
|
934
|
+
truncated: heads.truncated, source: isIntakeProvenanceSource(workItems) ? workItems : undefined });
|
|
935
|
+
}
|
|
936
|
+
#attentionPolicy() {
|
|
937
|
+
return Object.freeze({
|
|
938
|
+
minSample: this.config.attentionTuningMinSample,
|
|
939
|
+
negativeRatePpm: this.config.attentionTuningNegativeRatePpm,
|
|
940
|
+
muteRatePpm: this.config.attentionTuningMuteRatePpm,
|
|
941
|
+
regressionMarginPpm: this.config.attentionTuningRegressionMarginPpm,
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
#looseningOptions() {
|
|
945
|
+
return this.config.attentionLooseningEnabled ? Object.freeze({ replayWindowMs: this.config.attentionLooseningReplayWindowMs }) : undefined;
|
|
946
|
+
}
|
|
947
|
+
#attentionCohort(input) {
|
|
948
|
+
let cohort;
|
|
949
|
+
try {
|
|
950
|
+
cohort = normalizeAttentionCohort(input);
|
|
951
|
+
}
|
|
952
|
+
catch {
|
|
953
|
+
throw new AssistantEvaluationError('invalid-input', 'attention cohort is invalid');
|
|
954
|
+
}
|
|
955
|
+
if (!this.attentionCohorts.some(item => item.lane === cohort.lane && item.sourceId === cohort.sourceId)) {
|
|
956
|
+
throw new AssistantEvaluationError('not-found', 'attention cohort is not configured');
|
|
957
|
+
}
|
|
958
|
+
return cohort;
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* WP22 slice 2 deterministic tuning tick: for every configured cohort and
|
|
962
|
+
* every scope that has notifications in it, monitor an observing version
|
|
963
|
+
* (exact-parent rollback on regression) or adopt one bounded tightening
|
|
964
|
+
* step. With `attentionLooseningEnabled`, a pending sealed loosening
|
|
965
|
+
* proposal is decided first and tightening uses training data only.
|
|
966
|
+
* Zero work when `attentionTuningAutoAdopt` is off.
|
|
967
|
+
*/
|
|
968
|
+
reconcileAttentionTuning() {
|
|
969
|
+
this.assertActive();
|
|
970
|
+
if (!this.config.attentionTuningAutoAdopt)
|
|
971
|
+
return Object.freeze([]);
|
|
972
|
+
const policy = this.#attentionPolicy();
|
|
973
|
+
const loosening = this.#looseningOptions();
|
|
974
|
+
const options = {
|
|
975
|
+
observationMs: this.config.attentionTuningObservationMs, evaluationWindowMs: this.config.attentionTuningEvaluationWindowMs,
|
|
976
|
+
...(loosening === undefined ? {} : { loosening }),
|
|
977
|
+
};
|
|
978
|
+
const results = [];
|
|
979
|
+
for (const cohort of this.attentionCohorts) {
|
|
980
|
+
for (const scope of this.store.attention.scopes(cohort)) {
|
|
981
|
+
let scopeKey;
|
|
982
|
+
try {
|
|
983
|
+
scopeKey = canonicalEvaluationScope(scope).scopeKey;
|
|
984
|
+
}
|
|
985
|
+
catch {
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
988
|
+
results.push({ scopeKey, cohort, result: this.store.attention.tick(scope, cohort, policy, options) });
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
return Object.freeze(results);
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Verified active attention parameters for a consumer, or undefined when
|
|
995
|
+
* auto-adoption is off (consumers then apply no overlay at all).
|
|
996
|
+
*/
|
|
997
|
+
currentAttentionTuning(input) {
|
|
998
|
+
this.assertActive();
|
|
999
|
+
if (!this.config.attentionTuningAutoAdopt)
|
|
1000
|
+
return undefined;
|
|
1001
|
+
return this.store.attention.current(input.scope, this.#attentionCohort(input.cohort));
|
|
1002
|
+
}
|
|
1003
|
+
/** Versions, durable observation state and recent events for one cohort. */
|
|
1004
|
+
attentionTuningStatus(input) {
|
|
1005
|
+
this.assertActive();
|
|
1006
|
+
return this.store.attention.status(input.scope, this.#attentionCohort(input.cohort));
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Host-internal explicit request. Without `attentionLooseningEnabled`,
|
|
1010
|
+
* loosening is rejected as `loosening-requires-holdout` and recorded; with
|
|
1011
|
+
* it, a single-parameter loosening is sealed as a proposal for the next
|
|
1012
|
+
* tick's holdout replay and grant check.
|
|
1013
|
+
*/
|
|
1014
|
+
requestAttentionTuning(input) {
|
|
1015
|
+
this.assertActive();
|
|
1016
|
+
return this.store.attention.request(input.scope, this.#attentionCohort(input.cohort), input.parameters, {
|
|
1017
|
+
enabled: this.config.attentionTuningAutoAdopt,
|
|
1018
|
+
observationMs: this.config.attentionTuningObservationMs,
|
|
1019
|
+
evaluationWindowMs: this.config.attentionTuningEvaluationWindowMs,
|
|
1020
|
+
...(this.#looseningOptions() === undefined ? {} : { loosening: this.#looseningOptions() }),
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
/** Record an owner one-time loosening pre-authorization for one cohort. */
|
|
1024
|
+
grantAttentionLoosening(input) {
|
|
1025
|
+
this.assertActive();
|
|
1026
|
+
const cohort = this.#attentionCohort(input.cohort);
|
|
1027
|
+
if (!this.config.attentionLooseningEnabled)
|
|
1028
|
+
return Object.freeze({ status: 'rejected', reason: 'loosening-disabled' });
|
|
1029
|
+
return this.store.attention.grant(input.scope, cohort, input.grant);
|
|
1030
|
+
}
|
|
1031
|
+
/** Active grant with used/remaining quota, sealed proposals with decisions, and recorded owner questions. */
|
|
1032
|
+
attentionLooseningStatus(input) {
|
|
1033
|
+
this.assertActive();
|
|
1034
|
+
const status = this.store.attention.looseningStatus(input.scope, this.#attentionCohort(input.cohort));
|
|
1035
|
+
return Object.freeze({ enabled: this.config.attentionLooseningEnabled, ...status });
|
|
1036
|
+
}
|
|
1037
|
+
#ownerQuestionDelivery() {
|
|
1038
|
+
const delivery = this.ctx.get('assistantDelivery');
|
|
1039
|
+
return isOwnerQuestionDeliveryPort(delivery) ? delivery : undefined;
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Register (once per Delivery instance) the send-time withdrawal check for
|
|
1043
|
+
* owner-question notices. A Delivery without the check is treated as absent.
|
|
1044
|
+
*/
|
|
1045
|
+
bindOwnerQuestionWithdrawal(source) {
|
|
1046
|
+
if (!this.active || !isOwnerQuestionDeliveryPort(source))
|
|
1047
|
+
return undefined;
|
|
1048
|
+
if (this.ownerQuestionWithdrawal?.source === source)
|
|
1049
|
+
return undefined;
|
|
1050
|
+
const registration = Object.freeze({
|
|
1051
|
+
protocol: OWNER_NOTIFICATION_WITHDRAWAL_PROTOCOL,
|
|
1052
|
+
authority: 'assistantEvaluation',
|
|
1053
|
+
sourceId: ATTENTION_OWNER_QUESTION_SOURCE_ID,
|
|
1054
|
+
isWithdrawn: (idempotencyKey) => this.#ownerQuestionWithdrawn(idempotencyKey),
|
|
1055
|
+
});
|
|
1056
|
+
this.activeOwnerQuestionWithdrawals.add(registration);
|
|
1057
|
+
let handle;
|
|
1058
|
+
let unregister;
|
|
1059
|
+
try {
|
|
1060
|
+
const result = source.registerOwnerNotificationWithdrawal(registration);
|
|
1061
|
+
handle = result?.handle;
|
|
1062
|
+
unregister = result?.unregister;
|
|
1063
|
+
}
|
|
1064
|
+
catch (error) {
|
|
1065
|
+
this.activeOwnerQuestionWithdrawals.delete(registration);
|
|
1066
|
+
// Unbound: the tick defers owner questions as delivery-unavailable until a later bind succeeds.
|
|
1067
|
+
this.ctx.logger.warn(`assistant-evaluation: owner-question withdrawal registration failed; owner questions deferred: ${String(error)}`);
|
|
1068
|
+
return undefined;
|
|
1069
|
+
}
|
|
1070
|
+
if (typeof handle !== 'string' || handle.length === 0 || typeof unregister !== 'function') {
|
|
1071
|
+
this.activeOwnerQuestionWithdrawals.delete(registration);
|
|
1072
|
+
try {
|
|
1073
|
+
if (typeof unregister === 'function')
|
|
1074
|
+
unregister();
|
|
1075
|
+
}
|
|
1076
|
+
catch { /* best effort */ }
|
|
1077
|
+
this.ctx.logger.warn('assistant-evaluation: owner-question withdrawal registration returned no handle; owner questions deferred');
|
|
1078
|
+
return undefined;
|
|
1079
|
+
}
|
|
1080
|
+
const release = unregister;
|
|
1081
|
+
this.ownerQuestionWithdrawal?.dispose();
|
|
1082
|
+
let live = true;
|
|
1083
|
+
const binding = Object.freeze({ source, handle, dispose: () => {
|
|
1084
|
+
if (!live)
|
|
1085
|
+
return;
|
|
1086
|
+
live = false;
|
|
1087
|
+
this.activeOwnerQuestionWithdrawals.delete(registration);
|
|
1088
|
+
try {
|
|
1089
|
+
release();
|
|
1090
|
+
}
|
|
1091
|
+
catch { /* Delivery already gone */ }
|
|
1092
|
+
if (this.ownerQuestionWithdrawal === binding)
|
|
1093
|
+
this.ownerQuestionWithdrawal = undefined;
|
|
1094
|
+
} });
|
|
1095
|
+
this.ownerQuestionWithdrawal = binding;
|
|
1096
|
+
return binding.dispose;
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
* Send-time answer for one owner-question notice. It stays sendable only
|
|
1100
|
+
* while its question is still the cohort's latest, open, unanswered, inside
|
|
1101
|
+
* the notice TTL, and not recorded closed/expired. Anything else, including
|
|
1102
|
+
* a key this service did not issue, is withdrawn. A storage error throws so
|
|
1103
|
+
* Delivery holds the notice (bounded by its expiry) rather than sending.
|
|
1104
|
+
*/
|
|
1105
|
+
#ownerQuestionWithdrawn(idempotencyKey) {
|
|
1106
|
+
if (!this.active || !this.config.attentionOwnerQuestionDeliveryEnabled)
|
|
1107
|
+
return true;
|
|
1108
|
+
const prefix = attentionOwnerQuestionIdempotencyKey('');
|
|
1109
|
+
if (typeof idempotencyKey !== 'string' || !idempotencyKey.startsWith(prefix))
|
|
1110
|
+
return true;
|
|
1111
|
+
const questionId = idempotencyKey.slice(prefix.length);
|
|
1112
|
+
for (const cohort of this.attentionCohorts) {
|
|
1113
|
+
for (const scope of this.store.attention.scopes(cohort)) {
|
|
1114
|
+
const latest = this.store.attention.latestOwnerQuestion(scope, cohort);
|
|
1115
|
+
if (latest === undefined || latest === 'unverifiable' || latest.questionId !== questionId)
|
|
1116
|
+
continue;
|
|
1117
|
+
const delivery = this.store.attentionQuestions.delivery(questionId);
|
|
1118
|
+
return !latest.open || this.store.attentionQuestions.reply(questionId) !== undefined
|
|
1119
|
+
|| (delivery !== undefined && delivery.status !== 'queued')
|
|
1120
|
+
|| this.now() >= latest.question.createdAt + this.config.attentionOwnerQuestionNoticeTtlMs;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
return true;
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* WP22 slice 4 tick step: for each configured cohort and scope, the latest
|
|
1127
|
+
* recorded owner question is handed to Delivery at most once. An Outbox row
|
|
1128
|
+
* already holding the question's idempotency key is adopted as `queued`
|
|
1129
|
+
* before any other decision. Otherwise a question a newer grant (or a
|
|
1130
|
+
* recorded reply) already closed is recorded `closed` and never sent; one
|
|
1131
|
+
* older than the notice TTL is recorded `expired`. Notices are queued
|
|
1132
|
+
* `withdrawable`, so a question closed after queueing is withdrawn by
|
|
1133
|
+
* Delivery's send-time check instead of being sent. Delivery absence (or no
|
|
1134
|
+
* live withdrawal binding of this service to that Delivery instance), a
|
|
1135
|
+
* route that does not match the scope, or an enqueue failure leaves no row,
|
|
1136
|
+
* so a later tick retries under the same idempotency key. Zero work when
|
|
1137
|
+
* `attentionOwnerQuestionDeliveryEnabled` is off.
|
|
1138
|
+
*/
|
|
1139
|
+
reconcileAttentionOwnerQuestions() {
|
|
1140
|
+
this.assertActive();
|
|
1141
|
+
if (!this.config.attentionOwnerQuestionDeliveryEnabled)
|
|
1142
|
+
return Object.freeze([]);
|
|
1143
|
+
const owner = this.config.attentionOwnerQuestionOwner;
|
|
1144
|
+
const ttl = this.config.attentionOwnerQuestionNoticeTtlMs;
|
|
1145
|
+
const results = [];
|
|
1146
|
+
for (const cohort of this.attentionCohorts) {
|
|
1147
|
+
for (const scope of this.store.attention.scopes(cohort)) {
|
|
1148
|
+
let scopeKey;
|
|
1149
|
+
try {
|
|
1150
|
+
scopeKey = canonicalEvaluationScope(scope).scopeKey;
|
|
1151
|
+
}
|
|
1152
|
+
catch {
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1155
|
+
const push = (result) => { results.push({ scopeKey, cohort, result: Object.freeze(result) }); };
|
|
1156
|
+
const latest = this.store.attention.latestOwnerQuestion(scope, cohort);
|
|
1157
|
+
if (latest === undefined)
|
|
1158
|
+
continue;
|
|
1159
|
+
// Fail closed: a question whose stored body does not verify is never sent.
|
|
1160
|
+
if (latest === 'unverifiable') {
|
|
1161
|
+
push({ action: 'unverifiable' });
|
|
1162
|
+
continue;
|
|
1163
|
+
}
|
|
1164
|
+
const { questionId, question } = latest;
|
|
1165
|
+
if (this.store.attentionQuestions.delivery(questionId) !== undefined)
|
|
1166
|
+
continue;
|
|
1167
|
+
const delivery = this.#ownerQuestionDelivery();
|
|
1168
|
+
// Without Delivery no Outbox lookup is possible, so no terminal row is written either.
|
|
1169
|
+
if (delivery === undefined) {
|
|
1170
|
+
push({ action: 'deferred', questionId, reason: 'delivery-unavailable' });
|
|
1171
|
+
continue;
|
|
1172
|
+
}
|
|
1173
|
+
this.bindOwnerQuestionWithdrawal(delivery);
|
|
1174
|
+
// Only queue (or adopt) while this service's own withdrawal check is
|
|
1175
|
+
// bound to this exact Delivery instance; otherwise a closed question
|
|
1176
|
+
// could be sent unchecked.
|
|
1177
|
+
const withdrawal = this.ownerQuestionWithdrawal;
|
|
1178
|
+
if (withdrawal === undefined || withdrawal.source !== delivery) {
|
|
1179
|
+
push({ action: 'deferred', questionId, reason: 'delivery-unavailable' });
|
|
1180
|
+
continue;
|
|
1181
|
+
}
|
|
1182
|
+
const withdrawalHandle = withdrawal.handle;
|
|
1183
|
+
const idempotencyKey = attentionOwnerQuestionIdempotencyKey(questionId);
|
|
1184
|
+
// An Outbox row under this key (e.g. enqueued before a crash, or under an
|
|
1185
|
+
// older TTL) is adopted as queued; the send-time withdrawal check then
|
|
1186
|
+
// keeps a since-closed question from being sent.
|
|
1187
|
+
let existing;
|
|
1188
|
+
try {
|
|
1189
|
+
existing = delivery.inspectOwnerNotification({ sourceId: ATTENTION_OWNER_QUESTION_SOURCE_ID, idempotencyKey, withdrawalHandle });
|
|
1190
|
+
}
|
|
1191
|
+
catch {
|
|
1192
|
+
push({ action: 'deferred', questionId, reason: 'delivery-unavailable' });
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
if (existing !== undefined) {
|
|
1196
|
+
if (typeof existing.outboxId !== 'string' || existing.outboxId.length === 0 || existing.outboxId.length > 256) {
|
|
1197
|
+
push({ action: 'deferred', questionId, reason: 'delivery-unavailable' });
|
|
1198
|
+
continue;
|
|
1199
|
+
}
|
|
1200
|
+
this.store.attentionQuestions.recordDelivery(scope, cohort, questionId, 'queued', existing.outboxId);
|
|
1201
|
+
push({ action: 'queued', questionId, outboxId: existing.outboxId });
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1204
|
+
if (!latest.open || this.store.attentionQuestions.reply(questionId) !== undefined) {
|
|
1205
|
+
this.store.attentionQuestions.recordDelivery(scope, cohort, questionId, 'closed', null);
|
|
1206
|
+
push({ action: 'closed', questionId });
|
|
1207
|
+
continue;
|
|
1208
|
+
}
|
|
1209
|
+
const expiresAt = question.createdAt + ttl;
|
|
1210
|
+
if (this.now() >= expiresAt) {
|
|
1211
|
+
this.store.attentionQuestions.recordDelivery(scope, cohort, questionId, 'expired', null);
|
|
1212
|
+
push({ action: 'expired', questionId });
|
|
1213
|
+
continue;
|
|
1214
|
+
}
|
|
1215
|
+
let principal;
|
|
1216
|
+
let sessionId;
|
|
1217
|
+
try {
|
|
1218
|
+
const receipt = delivery.validateOwnerRoute({ authorityId: owner.ownerRouteId, principalId: owner.principalId,
|
|
1219
|
+
workspace: scope.workspace, agentPreset: scope.preset });
|
|
1220
|
+
sessionId = delivery.resolveOwnerRoute(owner.ownerRouteId).binding.sessionId;
|
|
1221
|
+
principal = { principalRecordId: receipt.principalRecordId, principalVersion: receipt.principalVersion };
|
|
1222
|
+
}
|
|
1223
|
+
catch {
|
|
1224
|
+
push({ action: 'deferred', questionId, reason: 'route-mismatch' });
|
|
1225
|
+
continue;
|
|
1226
|
+
}
|
|
1227
|
+
let outboxId;
|
|
1228
|
+
try {
|
|
1229
|
+
outboxId = delivery.enqueueOwnerNotification({
|
|
1230
|
+
sourceId: ATTENTION_OWNER_QUESTION_SOURCE_ID,
|
|
1231
|
+
ownerRouteId: owner.ownerRouteId,
|
|
1232
|
+
scope: { principalId: owner.principalId, ...principal, workspace: scope.workspace, preset: scope.preset },
|
|
1233
|
+
sessionId,
|
|
1234
|
+
idempotencyKey,
|
|
1235
|
+
text: attentionOwnerQuestionText(question, scope),
|
|
1236
|
+
expiresAt,
|
|
1237
|
+
attention: { purpose: 'result' },
|
|
1238
|
+
withdrawable: true,
|
|
1239
|
+
withdrawalHandle,
|
|
1240
|
+
}).id;
|
|
1241
|
+
}
|
|
1242
|
+
catch {
|
|
1243
|
+
push({ action: 'deferred', questionId, reason: 'enqueue-failed' });
|
|
1244
|
+
continue;
|
|
1245
|
+
}
|
|
1246
|
+
if (typeof outboxId !== 'string' || outboxId.length === 0 || outboxId.length > 256) {
|
|
1247
|
+
push({ action: 'deferred', questionId, reason: 'enqueue-failed' });
|
|
1248
|
+
continue;
|
|
1249
|
+
}
|
|
1250
|
+
this.store.attentionQuestions.recordDelivery(scope, cohort, questionId, 'queued', outboxId);
|
|
1251
|
+
push({ action: 'queued', questionId, outboxId });
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
return Object.freeze(results);
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Host-internal record-only owner reply to the cohort's latest question.
|
|
1258
|
+
* It never records a grant or changes a tuning version; an owner who
|
|
1259
|
+
* authorizes still records a grant through `grantAttentionLoosening`.
|
|
1260
|
+
*/
|
|
1261
|
+
recordAttentionOwnerQuestionReply(input) {
|
|
1262
|
+
this.assertActive();
|
|
1263
|
+
const cohort = this.#attentionCohort(input.cohort);
|
|
1264
|
+
if (!this.config.attentionOwnerQuestionDeliveryEnabled)
|
|
1265
|
+
return Object.freeze({ status: 'rejected', reason: 'delivery-disabled' });
|
|
1266
|
+
const latest = this.store.attention.latestOwnerQuestion(input.scope, cohort);
|
|
1267
|
+
if (latest === undefined || latest === 'unverifiable' || latest.questionId !== input.questionId) {
|
|
1268
|
+
return Object.freeze({ status: 'rejected', reason: 'unknown-question' });
|
|
1269
|
+
}
|
|
1270
|
+
// A question whose notice expired (recorded, or past the TTL) no longer takes a reply.
|
|
1271
|
+
const expired = this.store.attentionQuestions.delivery(latest.questionId)?.status === 'expired'
|
|
1272
|
+
|| this.now() >= latest.question.createdAt + this.config.attentionOwnerQuestionNoticeTtlMs;
|
|
1273
|
+
return this.store.attentionQuestions.recordReply(input.scope, cohort, latest.questionId, input.choice, latest.open, expired);
|
|
1274
|
+
}
|
|
1275
|
+
/** Per-question delivery decisions and recorded replies for one cohort. */
|
|
1276
|
+
attentionOwnerQuestionStatus(input) {
|
|
1277
|
+
this.assertActive();
|
|
1278
|
+
return this.store.attentionQuestions.status(input.scope, this.#attentionCohort(input.cohort), this.config.attentionOwnerQuestionDeliveryEnabled);
|
|
1279
|
+
}
|
|
703
1280
|
limits() {
|
|
704
1281
|
this.assertActive();
|
|
705
1282
|
return Object.freeze({
|
|
@@ -717,6 +1294,15 @@ export class AssistantEvaluationService extends Service {
|
|
|
717
1294
|
return this.active && typeof registration === 'object' && registration !== null
|
|
718
1295
|
&& this.activeAutomationRegistrations.has(registration);
|
|
719
1296
|
}
|
|
1297
|
+
/**
|
|
1298
|
+
* Delivery's slot-anchored proof that an owner-notification withdrawal
|
|
1299
|
+
* registration under the `assistantEvaluation` authority is this live
|
|
1300
|
+
* service's own; a foreign object claiming that authority never passes.
|
|
1301
|
+
*/
|
|
1302
|
+
ownsOwnerNotificationWithdrawal(registration) {
|
|
1303
|
+
return this.active && typeof registration === 'object' && registration !== null
|
|
1304
|
+
&& this.activeOwnerQuestionWithdrawals.has(registration);
|
|
1305
|
+
}
|
|
720
1306
|
/** Delivery uses the same ownership boundary as Automations. */
|
|
721
1307
|
ownsTrustedDeliveryEvaluationRegistration(registration) {
|
|
722
1308
|
return this.active && typeof registration === 'object' && registration !== null
|
|
@@ -726,6 +1312,11 @@ export class AssistantEvaluationService extends Service {
|
|
|
726
1312
|
return this.active && typeof registration === 'object' && registration !== null
|
|
727
1313
|
&& this.activeVerifierRegistrations.has(registration);
|
|
728
1314
|
}
|
|
1315
|
+
/** Work-items exports settled outcome revisions through the same ownership boundary. */
|
|
1316
|
+
ownsTrustedWorkItemEvaluationRegistration(registration) {
|
|
1317
|
+
return this.active && typeof registration === 'object' && registration !== null
|
|
1318
|
+
&& this.activeWorkItemRegistrations.has(registration);
|
|
1319
|
+
}
|
|
729
1320
|
review(agent, input = {}) {
|
|
730
1321
|
this.assertActive();
|
|
731
1322
|
const scope = this.agentScope(agent, 'evaluation_review');
|
|
@@ -871,7 +1462,22 @@ export class AssistantEvaluationService extends Service {
|
|
|
871
1462
|
if (current?.generation === generation)
|
|
872
1463
|
return current.dispose;
|
|
873
1464
|
const capabilities = new WeakMap();
|
|
1465
|
+
const workItemProofs = new WeakMap();
|
|
874
1466
|
let registered = true;
|
|
1467
|
+
// A WorkItems resolver is foreign code and can synchronously withdraw
|
|
1468
|
+
// this Delivery capability. Check generation first, then our own fields
|
|
1469
|
+
// after that last producer callback, directly before local mutation.
|
|
1470
|
+
const assertWorkItemDeliveryLocal = () => {
|
|
1471
|
+
this.assertActive();
|
|
1472
|
+
if (!registered || this.deliveryBinding?.producer !== producer || this.deliveryBinding.generation !== generation
|
|
1473
|
+
|| !this.activeDeliveryRegistrations.has(registration)) {
|
|
1474
|
+
throw new AssistantEvaluationError('forbidden', 'stale delivery Evaluation producer capability');
|
|
1475
|
+
}
|
|
1476
|
+
};
|
|
1477
|
+
const assertWorkItemDeliveryCurrent = () => {
|
|
1478
|
+
this.assertCurrentProducer(producer, generation, registered, 'delivery');
|
|
1479
|
+
assertWorkItemDeliveryLocal();
|
|
1480
|
+
};
|
|
875
1481
|
const registration = Object.freeze({
|
|
876
1482
|
protocol: TRUSTED_EVALUATION_PRODUCER_PROTOCOL,
|
|
877
1483
|
producer: 'assistant-delivery',
|
|
@@ -881,16 +1487,26 @@ export class AssistantEvaluationService extends Service {
|
|
|
881
1487
|
issueCapability: (claims) => {
|
|
882
1488
|
this.assertCurrentProducer(producer, generation, registered, 'delivery');
|
|
883
1489
|
const normalized = this.deliveryClaims(claims);
|
|
1490
|
+
let workItemProofDigest;
|
|
884
1491
|
if (normalized.subjectKind === 'goal-outcome') {
|
|
885
1492
|
const { proof, verifierOutcome } = this.resolveGoalOutcomeOwnerProof(normalized);
|
|
886
1493
|
if (this.store.adoptTrustedGoalOutcomeOwnerBaseline(normalized, proof, verifierOutcome)) {
|
|
887
1494
|
this.notifyTrustedTaskChange();
|
|
888
1495
|
}
|
|
889
1496
|
}
|
|
1497
|
+
else if (normalized.subjectKind === 'work-item') {
|
|
1498
|
+
if (typeof producer.withOwnerWorkItemFeedbackWriterFence !== 'function') {
|
|
1499
|
+
throw new AssistantEvaluationError('forbidden', 'Delivery work-item feedback writer fence is unavailable');
|
|
1500
|
+
}
|
|
1501
|
+
workItemProofDigest = this.resolveWorkItemOwnerProof(normalized).proofDigest;
|
|
1502
|
+
assertWorkItemDeliveryCurrent();
|
|
1503
|
+
}
|
|
890
1504
|
else
|
|
891
1505
|
this.store.adoptLegacyOwnerFeedback(normalized);
|
|
892
1506
|
const capability = Object.freeze(Object.create(null));
|
|
893
1507
|
capabilities.set(capability, normalized);
|
|
1508
|
+
if (workItemProofDigest !== undefined)
|
|
1509
|
+
workItemProofs.set(capability, workItemProofDigest);
|
|
894
1510
|
return capability;
|
|
895
1511
|
},
|
|
896
1512
|
inspect: (capabilityReceipt) => {
|
|
@@ -902,6 +1518,12 @@ export class AssistantEvaluationService extends Service {
|
|
|
902
1518
|
throw new AssistantEvaluationError('forbidden', 'missing owner lineage');
|
|
903
1519
|
if (claims.subjectKind === 'goal-outcome')
|
|
904
1520
|
this.resolveGoalOutcomeOwnerProof(claims);
|
|
1521
|
+
if (claims.subjectKind === 'work-item'
|
|
1522
|
+
&& this.resolveWorkItemOwnerProof(claims).proofDigest !== workItemProofs.get(capabilityReceipt)) {
|
|
1523
|
+
throw new AssistantEvaluationError('forbidden', 'work-item proof changed since owner capability issue');
|
|
1524
|
+
}
|
|
1525
|
+
if (claims.subjectKind === 'work-item')
|
|
1526
|
+
assertWorkItemDeliveryCurrent();
|
|
905
1527
|
return this.store.ownerObjectiveState(claims.scope, claims.subjectKind ?? 'automation-run', claims.subjectRef ?? claims.runId, claims.ownerCommand.principalRecordId, claims.ownerCommand.principalVersion);
|
|
906
1528
|
},
|
|
907
1529
|
append: (input) => {
|
|
@@ -924,6 +1546,59 @@ export class AssistantEvaluationService extends Service {
|
|
|
924
1546
|
}
|
|
925
1547
|
if (claims.subjectKind === 'goal-outcome')
|
|
926
1548
|
this.resolveGoalOutcomeOwnerProof(claims);
|
|
1549
|
+
if (claims.subjectKind === 'work-item') {
|
|
1550
|
+
const proof = this.resolveWorkItemOwnerProof(claims);
|
|
1551
|
+
assertWorkItemDeliveryCurrent();
|
|
1552
|
+
if (proof.proofDigest !== workItemProofs.get(input.capabilityReceipt))
|
|
1553
|
+
throw new AssistantEvaluationError('forbidden', 'work-item proof changed since owner capability issue');
|
|
1554
|
+
// Production termination is its own trusted fact. It is persisted
|
|
1555
|
+
// with objective unknown before the separately authenticated owner vote.
|
|
1556
|
+
this.store.append({
|
|
1557
|
+
scope: claims.scope, situation: claims.situation,
|
|
1558
|
+
executionStatus: proof.production.executionStatus, objectiveStatus: 'unknown', deliveryStatus: 'unknown',
|
|
1559
|
+
source: { kind: 'system', id: 'assistant-work-items-owner-proof' }, trust: 'trusted',
|
|
1560
|
+
evidence: [
|
|
1561
|
+
{ kind: 'work-item-owner-result', ref: proof.itemId },
|
|
1562
|
+
{ kind: 'work-item-report', ref: `${proof.itemId}#${String(proof.reportRevision)}`, digest: proof.reportDigest },
|
|
1563
|
+
{ kind: 'work-item-production-proof', ref: proof.production.runId, digest: proof.proofDigest },
|
|
1564
|
+
], metrics: {}, occurredAt: proof.production.completedAt,
|
|
1565
|
+
idempotencyKey: `work-item-owner-proof:${acceptanceDigest([proof.itemId, proof.proofDigest])}`,
|
|
1566
|
+
evaluator: { id: 'assistant-work-items-owner-proof', version: '1' },
|
|
1567
|
+
});
|
|
1568
|
+
// The native producer can run callbacks. Re-prove it after durable
|
|
1569
|
+
// baseline creation and directly before the owner mutation.
|
|
1570
|
+
const finalProof = this.resolveWorkItemOwnerProof(claims);
|
|
1571
|
+
assertWorkItemDeliveryCurrent();
|
|
1572
|
+
if (acceptanceDigest(finalProof) !== acceptanceDigest(proof))
|
|
1573
|
+
throw new AssistantEvaluationError('forbidden', 'work-item proof changed before owner append');
|
|
1574
|
+
if (typeof producer.withOwnerWorkItemFeedbackWriterFence !== 'function') {
|
|
1575
|
+
throw new AssistantEvaluationError('forbidden', 'Delivery work-item feedback writer fence is unavailable');
|
|
1576
|
+
}
|
|
1577
|
+
const outcome = producer.withOwnerWorkItemFeedbackWriterFence(registration, claims, finalProof.proofDigest, () => {
|
|
1578
|
+
// This callback runs while Delivery owns its source writer lock.
|
|
1579
|
+
// No producer calls or notifications may re-enter after its final
|
|
1580
|
+
// owner/source/Policy check and before this local owner mutation.
|
|
1581
|
+
assertWorkItemDeliveryLocal();
|
|
1582
|
+
return this.store.append({
|
|
1583
|
+
scope: claims.scope, situation: claims.situation,
|
|
1584
|
+
executionStatus: proof.production.executionStatus, objectiveStatus: claims.objectiveStatus,
|
|
1585
|
+
deliveryStatus: 'delivered', source: { kind: 'user-feedback', id: 'assistant-delivery/typed-owner-feedback' }, trust: 'trusted',
|
|
1586
|
+
evidence: [
|
|
1587
|
+
{ kind: 'work-item-owner-result', ref: proof.itemId },
|
|
1588
|
+
{ kind: 'work-item-report', ref: `${proof.itemId}#${String(proof.reportRevision)}`, digest: proof.reportDigest },
|
|
1589
|
+
{ kind: 'delivery-outbox', ref: claims.outboxId },
|
|
1590
|
+
{ kind: 'work-item-production-proof', ref: proof.production.runId, digest: proof.proofDigest },
|
|
1591
|
+
], metrics: {}, occurredAt: claims.occurredAt, idempotencyKey: claims.idempotencyKey,
|
|
1592
|
+
evaluator: { id: 'assistant-delivery-owner-feedback', version: '2' },
|
|
1593
|
+
}, claims.ownerCommand);
|
|
1594
|
+
});
|
|
1595
|
+
// Delivery has now committed its recorded source acknowledgement.
|
|
1596
|
+
// A lost cross-database acknowledgement remains a durable unknown;
|
|
1597
|
+
// it must recover the same operation instead of replaying a new vote.
|
|
1598
|
+
assertWorkItemDeliveryLocal();
|
|
1599
|
+
this.notifyTrustedTaskChange();
|
|
1600
|
+
return outcome;
|
|
1601
|
+
}
|
|
927
1602
|
return this.appendTrusted({
|
|
928
1603
|
scope: claims.scope,
|
|
929
1604
|
situation: claims.situation,
|
|
@@ -1086,6 +1761,114 @@ export class AssistantEvaluationService extends Service {
|
|
|
1086
1761
|
previous?.dispose();
|
|
1087
1762
|
return dispose;
|
|
1088
1763
|
}
|
|
1764
|
+
/**
|
|
1765
|
+
* Bind the settled work-item producer.
|
|
1766
|
+
*
|
|
1767
|
+
* The envelope's objective and delivery status are always `unknown` and its
|
|
1768
|
+
* execution status is derived here from the evidence state, never read from
|
|
1769
|
+
* the producer. A work item can therefore contribute durable evidence without
|
|
1770
|
+
* ever upgrading its own objective claim through Evaluation.
|
|
1771
|
+
*/
|
|
1772
|
+
bindWorkItemsProducer(producer) {
|
|
1773
|
+
const generation = hostIdentifier(producer.trustedWorkItemProducerGeneration(), 'trusted work-item generation', 200);
|
|
1774
|
+
const current = this.workItemsBinding;
|
|
1775
|
+
if (current?.generation === generation)
|
|
1776
|
+
return current.dispose;
|
|
1777
|
+
let registered = true;
|
|
1778
|
+
let disposeRegistration;
|
|
1779
|
+
let binding;
|
|
1780
|
+
const assertCurrent = () => {
|
|
1781
|
+
this.assertActive();
|
|
1782
|
+
if (!registered || this.workItemsBinding?.producer !== producer
|
|
1783
|
+
|| this.workItemsBinding.generation !== generation
|
|
1784
|
+
|| producer.trustedWorkItemProducerGeneration() !== generation) {
|
|
1785
|
+
throw new AssistantEvaluationError('forbidden', 'stale work-item Evaluation producer capability');
|
|
1786
|
+
}
|
|
1787
|
+
};
|
|
1788
|
+
const registration = Object.freeze({
|
|
1789
|
+
protocol: 'assistant-work-items/evaluation/v1',
|
|
1790
|
+
generation,
|
|
1791
|
+
owner: this,
|
|
1792
|
+
append: async (claims) => {
|
|
1793
|
+
assertCurrent();
|
|
1794
|
+
if (typeof claims !== 'object' || claims === null || Array.isArray(claims)) {
|
|
1795
|
+
throw new AssistantEvaluationError('forbidden', 'work-item Evaluation claims are invalid');
|
|
1796
|
+
}
|
|
1797
|
+
const itemId = hostIdentifier(claims.itemId, 'work item id', 200);
|
|
1798
|
+
const revision = claims.outcomeRevision;
|
|
1799
|
+
const settledBy = claims.settledBy;
|
|
1800
|
+
const evidenceState = claims.evidenceState;
|
|
1801
|
+
const occurredAt = claims.occurredAt;
|
|
1802
|
+
if (!Number.isSafeInteger(revision) || revision < 1
|
|
1803
|
+
|| !['report-ready', 'report-blocked', 'report-failed', 'withdrawn', 'retry-exhausted', 'observed-evidence', 'window-expired', 'closed-by-thread'].includes(settledBy)
|
|
1804
|
+
|| !['observed', 'unverified', 'not-applicable'].includes(evidenceState)) {
|
|
1805
|
+
throw new AssistantEvaluationError('forbidden', 'work-item Evaluation settlement is invalid');
|
|
1806
|
+
}
|
|
1807
|
+
// The registration type already limits these to the literal `'unknown'`;
|
|
1808
|
+
// re-proving them at runtime keeps a forged capability from widening it.
|
|
1809
|
+
if (claims.objectiveStatus !== 'unknown' || claims.deliveryStatus !== 'unknown') {
|
|
1810
|
+
throw new AssistantEvaluationError('forbidden', 'work-item Evaluation claims cannot assert an objective');
|
|
1811
|
+
}
|
|
1812
|
+
const idempotencyKey = `work-items/${itemId}/${String(revision)}/${evidenceState}`;
|
|
1813
|
+
if (claims.idempotencyKey !== idempotencyKey) {
|
|
1814
|
+
throw new AssistantEvaluationError('forbidden', 'work-item Evaluation identity is not deterministic');
|
|
1815
|
+
}
|
|
1816
|
+
const digest = hostIdentifier(claims.digest, 'work-item outcome digest', 200);
|
|
1817
|
+
const situation = hostIdentifier(claims.situation, 'work-item situation', this.config.maxSituationBytes);
|
|
1818
|
+
if (!Number.isSafeInteger(occurredAt) || occurredAt < 0 || occurredAt > this.now()) {
|
|
1819
|
+
throw new AssistantEvaluationError('forbidden', 'work-item Evaluation occurrence is invalid');
|
|
1820
|
+
}
|
|
1821
|
+
const scope = canonicalEvaluationScope(claims.scope).scope;
|
|
1822
|
+
this.appendTrusted({
|
|
1823
|
+
scope,
|
|
1824
|
+
situation,
|
|
1825
|
+
executionStatus: evidenceState === 'observed' ? 'succeeded' : 'unknown',
|
|
1826
|
+
objectiveStatus: 'unknown',
|
|
1827
|
+
deliveryStatus: 'unknown',
|
|
1828
|
+
source: { kind: 'system', id: 'assistant-work-items' },
|
|
1829
|
+
trust: 'trusted',
|
|
1830
|
+
evidence: [
|
|
1831
|
+
{ kind: 'work-item', ref: itemId },
|
|
1832
|
+
{ kind: 'work-item-outcome', ref: `${itemId}#${String(revision)}`, digest },
|
|
1833
|
+
],
|
|
1834
|
+
metrics: claims.metrics,
|
|
1835
|
+
occurredAt,
|
|
1836
|
+
idempotencyKey,
|
|
1837
|
+
evaluator: {
|
|
1838
|
+
id: 'assistant-work-items',
|
|
1839
|
+
version: evidenceState === 'observed' ? 'settlement-v1' : 'unverified-v1',
|
|
1840
|
+
},
|
|
1841
|
+
});
|
|
1842
|
+
},
|
|
1843
|
+
});
|
|
1844
|
+
this.activeWorkItemRegistrations.add(registration);
|
|
1845
|
+
const dispose = () => {
|
|
1846
|
+
if (!registered)
|
|
1847
|
+
return;
|
|
1848
|
+
registered = false;
|
|
1849
|
+
this.activeWorkItemRegistrations.delete(registration);
|
|
1850
|
+
if (this.workItemsBinding === binding)
|
|
1851
|
+
this.workItemsBinding = undefined;
|
|
1852
|
+
disposeRegistration?.();
|
|
1853
|
+
};
|
|
1854
|
+
binding = Object.freeze({ producer, generation, dispose });
|
|
1855
|
+
const previous = this.workItemsBinding;
|
|
1856
|
+
this.workItemsBinding = binding;
|
|
1857
|
+
try {
|
|
1858
|
+
disposeRegistration = producer.registerTrustedWorkItemEvaluationSink(registration);
|
|
1859
|
+
if (typeof disposeRegistration !== 'function')
|
|
1860
|
+
throw new AssistantEvaluationError('forbidden', 'work-items returned no registration disposer');
|
|
1861
|
+
}
|
|
1862
|
+
catch (error) {
|
|
1863
|
+
registered = false;
|
|
1864
|
+
this.activeWorkItemRegistrations.delete(registration);
|
|
1865
|
+
if (this.workItemsBinding === binding)
|
|
1866
|
+
this.workItemsBinding = previous;
|
|
1867
|
+
throw error;
|
|
1868
|
+
}
|
|
1869
|
+
previous?.dispose();
|
|
1870
|
+
return dispose;
|
|
1871
|
+
}
|
|
1089
1872
|
producerGeneration(producer) {
|
|
1090
1873
|
return hostIdentifier(producer.trustedEvaluationProducerGeneration(), 'trusted Evaluation producer generation', 200);
|
|
1091
1874
|
}
|
|
@@ -1146,7 +1929,7 @@ export class AssistantEvaluationService extends Service {
|
|
|
1146
1929
|
const subjectKind = input.subjectKind ?? 'automation-run';
|
|
1147
1930
|
const subjectRef = input.subjectRef ?? input.runId;
|
|
1148
1931
|
if ((input.subjectKind !== undefined && input.subjectKind !== 'automation-run'
|
|
1149
|
-
&& input.subjectKind !== 'foreground-turn' && input.subjectKind !== 'goal-outcome')
|
|
1932
|
+
&& input.subjectKind !== 'foreground-turn' && input.subjectKind !== 'goal-outcome' && input.subjectKind !== 'work-item')
|
|
1150
1933
|
|| (input.subjectRef !== undefined && input.subjectRef.trim() === '')) {
|
|
1151
1934
|
throw new AssistantEvaluationError('forbidden', 'delivery Evaluation subject is invalid');
|
|
1152
1935
|
}
|
|
@@ -1164,7 +1947,16 @@ export class AssistantEvaluationService extends Service {
|
|
|
1164
1947
|
|| typeof input.goalOutcomeCapability !== 'object' || input.goalOutcomeCapability === null)) {
|
|
1165
1948
|
throw new AssistantEvaluationError('forbidden', 'goal outcome owner capability is invalid');
|
|
1166
1949
|
}
|
|
1950
|
+
if (subjectKind === 'work-item' && (input.subjectRef === undefined || command === undefined
|
|
1951
|
+
|| typeof input.workItemOutcomeCapability !== 'object' || input.workItemOutcomeCapability === null)) {
|
|
1952
|
+
throw new AssistantEvaluationError('forbidden', 'work-item owner capability is invalid');
|
|
1953
|
+
}
|
|
1954
|
+
if ((subjectKind !== 'work-item' && Object.hasOwn(input, 'workItemOutcomeCapability'))
|
|
1955
|
+
|| (subjectKind !== 'goal-outcome' && Object.hasOwn(input, 'goalOutcomeCapability'))) {
|
|
1956
|
+
throw new AssistantEvaluationError('forbidden', 'owner capability does not match subject');
|
|
1957
|
+
}
|
|
1167
1958
|
return Object.freeze({
|
|
1959
|
+
...(subjectKind === 'work-item' ? { workItemOutcomeCapability: input.workItemOutcomeCapability } : {}),
|
|
1168
1960
|
...(subjectKind === 'goal-outcome'
|
|
1169
1961
|
? { goalOutcomeCapability: input.goalOutcomeCapability } : {}),
|
|
1170
1962
|
...(input.initialIdempotencyKey === undefined ? {} : { initialIdempotencyKey: hostIdentifier(input.initialIdempotencyKey, 'initial feedback key', 200) }),
|
|
@@ -1186,6 +1978,39 @@ export class AssistantEvaluationService extends Service {
|
|
|
1186
1978
|
idempotencyKey: hostIdentifier(input.idempotencyKey, 'idempotencyKey', 200),
|
|
1187
1979
|
});
|
|
1188
1980
|
}
|
|
1981
|
+
resolveWorkItemOwnerProof(claims) {
|
|
1982
|
+
this.assertActive();
|
|
1983
|
+
const producer = this.ctx.get('assistantWorkItems', false);
|
|
1984
|
+
const binding = this.workItemsBinding;
|
|
1985
|
+
if (claims.subjectKind !== 'work-item' || !isTrustedWorkItemEvaluationProducer(producer)
|
|
1986
|
+
|| !binding || serviceIdentity(producer) !== serviceIdentity(binding.producer)
|
|
1987
|
+
|| producer.trustedWorkItemProducerGeneration() !== binding.generation
|
|
1988
|
+
|| typeof producer.resolveOwnerWorkItemFeedbackTarget !== 'function') {
|
|
1989
|
+
throw new AssistantEvaluationError('forbidden', 'current WorkItems owner capability resolver is unavailable');
|
|
1990
|
+
}
|
|
1991
|
+
let proof;
|
|
1992
|
+
try {
|
|
1993
|
+
const value = producer.resolveOwnerWorkItemFeedbackTarget(claims.workItemOutcomeCapability);
|
|
1994
|
+
proof = validateWorkItemOwnerFeedbackProof(value, this.now());
|
|
1995
|
+
}
|
|
1996
|
+
catch {
|
|
1997
|
+
throw new AssistantEvaluationError('forbidden', 'WorkItems owner capability is stale or invalid');
|
|
1998
|
+
}
|
|
1999
|
+
const currentGeneration = producer.trustedWorkItemProducerGeneration();
|
|
2000
|
+
const current = this.ctx.get('assistantWorkItems', false);
|
|
2001
|
+
this.assertActive();
|
|
2002
|
+
if (currentGeneration !== binding.generation || !current || typeof current !== 'object' || this.workItemsBinding !== binding
|
|
2003
|
+
|| serviceIdentity(current) !== serviceIdentity(producer)
|
|
2004
|
+
|| claims.subjectRef !== proof.itemId || claims.runId !== proof.production.runId
|
|
2005
|
+
|| claims.scope.workspace !== proof.scope.workspace || claims.scope.preset !== proof.scope.preset
|
|
2006
|
+
|| claims.situation !== `work-item:${proof.itemKind}` || claims.principalId !== proof.owner.principalId
|
|
2007
|
+
|| !claims.ownerCommand || claims.ownerCommand.principalRecordId !== proof.owner.principalRecordId
|
|
2008
|
+
|| claims.ownerCommand.principalVersion !== proof.owner.principalVersion
|
|
2009
|
+
|| claims.occurredAt < proof.report.recordedAt || claims.occurredAt > this.now()) {
|
|
2010
|
+
throw new AssistantEvaluationError('forbidden', 'WorkItems proof does not match current Delivery claims');
|
|
2011
|
+
}
|
|
2012
|
+
return proof;
|
|
2013
|
+
}
|
|
1189
2014
|
resolveGoalOutcomeOwnerProof(claims) {
|
|
1190
2015
|
const goals = this.ctx.get('assistantGoals', false);
|
|
1191
2016
|
if (!isTrustedGoalsOutcomeOwnerResolver(goals)) {
|