@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.
Files changed (80) hide show
  1. package/README.md +54 -1
  2. package/lib/attention-owner-questions.d.ts +85 -0
  3. package/lib/attention-owner-questions.d.ts.map +1 -0
  4. package/lib/attention-owner-questions.js +142 -0
  5. package/lib/attention-owner-questions.js.map +1 -0
  6. package/lib/attention-tuning.d.ts +431 -0
  7. package/lib/attention-tuning.d.ts.map +1 -0
  8. package/lib/attention-tuning.js +892 -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 +11 -6
  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.d.ts +15 -2
  35. package/lib/benchmark/native.d.ts.map +1 -1
  36. package/lib/benchmark/native.js +67 -14
  37. package/lib/benchmark/native.js.map +1 -1
  38. package/lib/benchmark/report.d.ts.map +1 -1
  39. package/lib/benchmark/report.js +98 -8
  40. package/lib/benchmark/report.js.map +1 -1
  41. package/lib/benchmark/runner.d.ts.map +1 -1
  42. package/lib/benchmark/runner.js +5 -0
  43. package/lib/benchmark/runner.js.map +1 -1
  44. package/lib/benchmark/types.d.ts +64 -7
  45. package/lib/benchmark/types.d.ts.map +1 -1
  46. package/lib/index.d.ts +7 -0
  47. package/lib/index.d.ts.map +1 -1
  48. package/lib/index.js +4 -0
  49. package/lib/index.js.map +1 -1
  50. package/lib/intake-attention-agreement.d.ts +63 -0
  51. package/lib/intake-attention-agreement.d.ts.map +1 -0
  52. package/lib/intake-attention-agreement.js +146 -0
  53. package/lib/intake-attention-agreement.js.map +1 -0
  54. package/lib/notification-outcomes.d.ts +56 -0
  55. package/lib/notification-outcomes.d.ts.map +1 -0
  56. package/lib/notification-outcomes.js +348 -0
  57. package/lib/notification-outcomes.js.map +1 -0
  58. package/lib/service.d.ts +244 -2
  59. package/lib/service.d.ts.map +1 -1
  60. package/lib/service.js +830 -5
  61. package/lib/service.js.map +1 -1
  62. package/lib/sqlite.d.ts +8 -2
  63. package/lib/sqlite.d.ts.map +1 -1
  64. package/lib/sqlite.js +406 -17
  65. package/lib/sqlite.js.map +1 -1
  66. package/lib/store.d.ts +13 -2
  67. package/lib/store.d.ts.map +1 -1
  68. package/lib/store.js +120 -24
  69. package/lib/store.js.map +1 -1
  70. package/lib/types.d.ts +247 -5
  71. package/lib/types.d.ts.map +1 -1
  72. package/lib/types.js +11 -0
  73. package/lib/types.js.map +1 -1
  74. package/lib/version.d.ts +1 -1
  75. package/lib/version.js +1 -1
  76. package/lib/work-item-owner-proof.d.ts +4 -0
  77. package/lib/work-item-owner-proof.d.ts.map +1 -0
  78. package/lib/work-item-owner-proof.js +53 -0
  79. package/lib/work-item-owner-proof.js.map +1 -0
  80. 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 === 'recorded') {
416
- return Object.freeze({ evaluationId, status: 'recorded', attemptCount: state.attemptCount });
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 === 'recorded' ? 'recorded' : 'deferred',
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)) {