@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.
Files changed (89) hide show
  1. package/README.md +69 -1
  2. package/lib/attention-owner-questions.d.ts +104 -0
  3. package/lib/attention-owner-questions.d.ts.map +1 -0
  4. package/lib/attention-owner-questions.js +179 -0
  5. package/lib/attention-owner-questions.js.map +1 -0
  6. package/lib/attention-tuning.d.ts +492 -0
  7. package/lib/attention-tuning.d.ts.map +1 -0
  8. package/lib/attention-tuning.js +1044 -0
  9. package/lib/attention-tuning.js.map +1 -0
  10. package/lib/benchmark/cli.d.ts.map +1 -1
  11. package/lib/benchmark/cli.js +14 -11
  12. package/lib/benchmark/cli.js.map +1 -1
  13. package/lib/benchmark/corpus.d.ts +13 -2
  14. package/lib/benchmark/corpus.d.ts.map +1 -1
  15. package/lib/benchmark/corpus.js +19 -9
  16. package/lib/benchmark/corpus.js.map +1 -1
  17. package/lib/benchmark/families-corpus.d.ts +11 -0
  18. package/lib/benchmark/families-corpus.d.ts.map +1 -0
  19. package/lib/benchmark/families-corpus.js +133 -0
  20. package/lib/benchmark/families-corpus.js.map +1 -0
  21. package/lib/benchmark/holdout-isolation.d.ts +27 -0
  22. package/lib/benchmark/holdout-isolation.d.ts.map +1 -0
  23. package/lib/benchmark/holdout-isolation.js +66 -0
  24. package/lib/benchmark/holdout-isolation.js.map +1 -0
  25. package/lib/benchmark/holdout.d.ts.map +1 -1
  26. package/lib/benchmark/holdout.js +3 -0
  27. package/lib/benchmark/holdout.js.map +1 -1
  28. package/lib/benchmark/index.d.ts +3 -0
  29. package/lib/benchmark/index.d.ts.map +1 -1
  30. package/lib/benchmark/index.js +2 -0
  31. package/lib/benchmark/index.js.map +1 -1
  32. package/lib/benchmark/native-skills.d.ts +12 -14
  33. package/lib/benchmark/native-skills.d.ts.map +1 -1
  34. package/lib/benchmark/native-skills.js +1 -3
  35. package/lib/benchmark/native-skills.js.map +1 -1
  36. package/lib/benchmark/native.d.ts +15 -2
  37. package/lib/benchmark/native.d.ts.map +1 -1
  38. package/lib/benchmark/native.js +67 -14
  39. package/lib/benchmark/native.js.map +1 -1
  40. package/lib/benchmark/report.d.ts.map +1 -1
  41. package/lib/benchmark/report.js +98 -8
  42. package/lib/benchmark/report.js.map +1 -1
  43. package/lib/benchmark/runner.d.ts.map +1 -1
  44. package/lib/benchmark/runner.js +5 -0
  45. package/lib/benchmark/runner.js.map +1 -1
  46. package/lib/benchmark/strategy-capabilities.d.ts.map +1 -1
  47. package/lib/benchmark/strategy-capabilities.js +0 -5
  48. package/lib/benchmark/strategy-capabilities.js.map +1 -1
  49. package/lib/benchmark/types.d.ts +64 -7
  50. package/lib/benchmark/types.d.ts.map +1 -1
  51. package/lib/index.d.ts +8 -1
  52. package/lib/index.d.ts.map +1 -1
  53. package/lib/index.js +4 -0
  54. package/lib/index.js.map +1 -1
  55. package/lib/intake-attention-agreement.d.ts +63 -0
  56. package/lib/intake-attention-agreement.d.ts.map +1 -0
  57. package/lib/intake-attention-agreement.js +146 -0
  58. package/lib/intake-attention-agreement.js.map +1 -0
  59. package/lib/notification-outcomes.d.ts +56 -0
  60. package/lib/notification-outcomes.d.ts.map +1 -0
  61. package/lib/notification-outcomes.js +348 -0
  62. package/lib/notification-outcomes.js.map +1 -0
  63. package/lib/service.d.ts +295 -2
  64. package/lib/service.d.ts.map +1 -1
  65. package/lib/service.js +965 -5
  66. package/lib/service.js.map +1 -1
  67. package/lib/sqlite.d.ts +8 -2
  68. package/lib/sqlite.d.ts.map +1 -1
  69. package/lib/sqlite.js +406 -17
  70. package/lib/sqlite.js.map +1 -1
  71. package/lib/store.d.ts +13 -2
  72. package/lib/store.d.ts.map +1 -1
  73. package/lib/store.js +122 -24
  74. package/lib/store.js.map +1 -1
  75. package/lib/types.d.ts +247 -5
  76. package/lib/types.d.ts.map +1 -1
  77. package/lib/types.js +11 -0
  78. package/lib/types.js.map +1 -1
  79. package/lib/version.d.ts +1 -1
  80. package/lib/version.js +1 -1
  81. package/lib/work-item-owner-proof.d.ts +4 -0
  82. package/lib/work-item-owner-proof.d.ts.map +1 -0
  83. package/lib/work-item-owner-proof.js +53 -0
  84. package/lib/work-item-owner-proof.js.map +1 -0
  85. package/package.json +29 -38
  86. package/lib/benchmark/super-relay.d.ts +0 -26
  87. package/lib/benchmark/super-relay.d.ts.map +0 -1
  88. package/lib/benchmark/super-relay.js +0 -173
  89. 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 === 'recorded') {
416
- return Object.freeze({ evaluationId, status: 'recorded', attemptCount: state.attemptCount });
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 === 'recorded' ? 'recorded' : 'deferred',
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)) {