@dsh-enhanced/assistant-work-items 0.1.66 → 0.1.68

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 (53) hide show
  1. package/README.md +17 -10
  2. package/lib/acceptance-binding.d.ts +3 -1
  3. package/lib/acceptance-binding.d.ts.map +1 -1
  4. package/lib/acceptance-binding.js +26 -1
  5. package/lib/acceptance-binding.js.map +1 -1
  6. package/lib/authority.d.ts +7 -0
  7. package/lib/authority.d.ts.map +1 -1
  8. package/lib/authority.js +28 -0
  9. package/lib/authority.js.map +1 -1
  10. package/lib/continuation-contract.d.ts +1 -1
  11. package/lib/continuation-contract.d.ts.map +1 -1
  12. package/lib/continuation-contract.js +5 -2
  13. package/lib/continuation-contract.js.map +1 -1
  14. package/lib/continuation.d.ts.map +1 -1
  15. package/lib/continuation.js +7 -2
  16. package/lib/continuation.js.map +1 -1
  17. package/lib/dispatch.d.ts +6 -16
  18. package/lib/dispatch.d.ts.map +1 -1
  19. package/lib/dispatch.js +14 -18
  20. package/lib/dispatch.js.map +1 -1
  21. package/lib/intake-admission.d.ts +28 -2
  22. package/lib/intake-admission.d.ts.map +1 -1
  23. package/lib/intake-admission.js +22 -4
  24. package/lib/intake-admission.js.map +1 -1
  25. package/lib/intake.d.ts +38 -0
  26. package/lib/intake.d.ts.map +1 -1
  27. package/lib/intake.js +55 -1
  28. package/lib/intake.js.map +1 -1
  29. package/lib/mandate.d.ts.map +1 -1
  30. package/lib/mandate.js +4 -0
  31. package/lib/mandate.js.map +1 -1
  32. package/lib/owner-replies.d.ts.map +1 -1
  33. package/lib/owner-replies.js +37 -1
  34. package/lib/owner-replies.js.map +1 -1
  35. package/lib/peers.d.ts +2 -0
  36. package/lib/peers.d.ts.map +1 -1
  37. package/lib/reply-contract.d.ts +1 -1
  38. package/lib/reply-contract.d.ts.map +1 -1
  39. package/lib/reply-contract.js +2 -2
  40. package/lib/reply-contract.js.map +1 -1
  41. package/lib/service.d.ts +29 -35
  42. package/lib/service.d.ts.map +1 -1
  43. package/lib/service.js +177 -105
  44. package/lib/service.js.map +1 -1
  45. package/lib/store.d.ts +6 -4
  46. package/lib/store.d.ts.map +1 -1
  47. package/lib/store.js +15 -8
  48. package/lib/store.js.map +1 -1
  49. package/lib/types.d.ts +3 -1
  50. package/lib/types.d.ts.map +1 -1
  51. package/lib/version.d.ts +1 -1
  52. package/lib/version.js +1 -1
  53. package/package.json +7 -7
package/lib/service.js CHANGED
@@ -1,6 +1,7 @@
1
+ import { acceptanceDigest } from '@dsh-enhanced/task-acceptance-contract';
1
2
  import { WorkItemEngineeringTargets } from './engineering-context.js';
2
3
  import { Service } from '@deepseek-ai/cordis';
3
- import { classifyIntake, intakeSourcePolicy } from './intake.js';
4
+ import { classifyIntake, intakeAcceptanceOf, intakeObjectiveOf, intakeSourcePolicy, MAX_INTAKE_OBJECTIVE_BYTES } from './intake.js';
4
5
  import { authenticatesIntakeText, WorkItemIntakeEvaluationRuntime } from './intake-evaluation-runtime.js';
5
6
  import { WorkItemIntakeAdmissionBridge } from './intake-admission.js';
6
7
  import { WorkItemAdmission } from './admission.js';
@@ -14,7 +15,8 @@ import { settleOutcomes as runSettlement } from './settlement.js';
14
15
  import { WorkItemOwnerRecall } from './owner-recall.js';
15
16
  import { MALFORMED_ACCEPTANCE_BINDING, WorkItemStore } from './store.js';
16
17
  import { normalizeFollowUp, normalizePolicyChange, noticePolicyChange } from './policy-notice.js';
17
- import { resolveAcceptanceBinding, settleAcceptance, workerRunTaskRef } from './acceptance-binding.js';
18
+ import { parseStoredContinuationPlan } from './reply-contract.js';
19
+ import { hasFreshAcceptanceReceipt, resolveAcceptanceBinding, settleAcceptance, workerRunTaskRef } from './acceptance-binding.js';
18
20
  import { WorkItemSignalIntake, } from './signals.js';
19
21
  import { registerWorkItemTools, workItemReportKey } from './tools.js';
20
22
  import { TERMINAL_STATES, WorkItemError } from './types.js';
@@ -75,6 +77,10 @@ const WORKER_RUN_BIND_SKIP_LIMIT = 1_000;
75
77
  const WORKER_RUN_DRIFT_WARN_AFTER = 5;
76
78
  /** Held reports released per scheduler wake; a full page advances the in-memory round-robin cursor. */
77
79
  const HELD_REPORT_RELEASE_LIMIT = 10;
80
+ /** Meta cursor: last fully-noticed Policy configuration digest. */
81
+ const POLICY_NOTICE_VERSION_META = 'policy.notice.version';
82
+ /** Bound one wake's Policy-notice paging; an incomplete pass retries next wake. */
83
+ const POLICY_NOTICE_PAGE_LIMIT = 10;
78
84
  /**
79
85
  * `bindWorkerRun` refusals that cannot change while the item keeps its
80
86
  * occurrence: an expired, mismatched (ref, owner scope, digest), settled or
@@ -151,6 +157,9 @@ export class AssistantWorkItemsService extends Service {
151
157
  workItemsMalformedBindingWarned = new Set();
152
158
  workItemsOwnerReads;
153
159
  workItemsOwnerContinuations;
160
+ /** Bounded, memory-only producer hints for inferred auto admission; never persisted. */
161
+ workItemsIntakeHints = new Map();
162
+ static MAX_INTAKE_HINTS = 256;
154
163
  constructor(ctx, input) {
155
164
  super(ctx, 'assistantWorkItems');
156
165
  this.workItemsConfig = Object.freeze(normalizeConfig(input, Date.now()));
@@ -170,6 +179,13 @@ export class AssistantWorkItemsService extends Service {
170
179
  store: () => this.workItemsClosed ? undefined : this.workItemsComponents?.store,
171
180
  // The public Host path, so an admitted item wakes its worker like any decision.
172
181
  admit: decision => this.admitDecision(decision),
182
+ hint: (source, sourceEventId) => {
183
+ const hint = this.workItemsIntakeHints.get(this.intakeHintKey(source, sourceEventId));
184
+ if (hint?.objective === undefined)
185
+ return undefined;
186
+ return Object.freeze({ objective: hint.objective,
187
+ ...(hint.acceptance === undefined ? {} : { acceptance: hint.acceptance }) });
188
+ },
173
189
  log: message => ctx.logger.debug(message),
174
190
  });
175
191
  this.workItemsOwnerReads = new WorkItemOwnerRecall({ ...ownerDependencies, access });
@@ -543,6 +559,9 @@ export class AssistantWorkItemsService extends Service {
543
559
  catch (error) {
544
560
  this.ctx.logger.warn(`assistant-work-items: acceptance auto-settle scan failed: ${describe(error)}`);
545
561
  }
562
+ // P1-1: resolve outcome windows and followed-thread closures on the same
563
+ // bounded wake instead of waiting for an unrelated claim or Host call.
564
+ this.workItemsSettleQuietly(components);
546
565
  // Release only after auto-settle, so a verdict that arrived on this wake
547
566
  // decides the same pass whether the held report is sent or blocked.
548
567
  try {
@@ -551,9 +570,61 @@ export class AssistantWorkItemsService extends Service {
551
570
  catch (error) {
552
571
  this.ctx.logger.warn(`assistant-work-items: acceptance held-report scan failed: ${describe(error)}`);
553
572
  }
573
+ // P1-1: materialize terminal follow-up releases on the same bounded wake.
574
+ // The store derives them from durable state, so this is restart-safe and a
575
+ // second pass releases nothing new. Delivery still owns the authenticated
576
+ // reply text and decides whether a released follow-up re-enters judgment.
577
+ try {
578
+ const released = components.store.releaseEndedFollowUps(100);
579
+ if (released.length > 0) {
580
+ this.ctx.logger.debug(`assistant-work-items: released ${released.length} ended follow-up(s)`);
581
+ }
582
+ }
583
+ catch (error) {
584
+ this.ctx.logger.warn(`assistant-work-items: terminal follow-up release scan failed: ${describe(error)}`);
585
+ }
586
+ try {
587
+ this.workItemsNoticePolicyChange(components);
588
+ }
589
+ catch (error) {
590
+ this.ctx.logger.warn(`assistant-work-items: policy notice scan failed: ${describe(error)}`);
591
+ }
554
592
  this.workItemsSchedulerWaking = false;
555
593
  }
556
594
  }
595
+ /**
596
+ * P1-1: a changed Policy configuration notices every live item whose frozen
597
+ * mandate basis no longer matches. The digest is read from the detached Policy
598
+ * host snapshot; no Policy audit or budget is consumed. Notices are durable
599
+ * and idempotent, and this path never cancels, parks or wakes an item.
600
+ */
601
+ workItemsNoticePolicyChange(components) {
602
+ const version = components.authority.policyVersion();
603
+ if (version === null || components.store.meta(POLICY_NOTICE_VERSION_META) === version)
604
+ return;
605
+ let after;
606
+ for (let page = 0; page < POLICY_NOTICE_PAGE_LIMIT; page += 1) {
607
+ const result = noticePolicyChange(components.store, components.authority, {
608
+ protocol: 'work-items/policy-change/v1', policyVersion: version,
609
+ ...(after === undefined ? {} : { after }),
610
+ });
611
+ if (result.outcome !== 'noticed')
612
+ return;
613
+ if (result.next === null) {
614
+ // A transient Policy failure leaves affected items unassessed. Do not
615
+ // record the version as complete: the next wake must retry the same
616
+ // configuration so those items still get their notices after recovery.
617
+ if (result.unavailable.length > 0)
618
+ return;
619
+ components.store.setMeta(POLICY_NOTICE_VERSION_META, version);
620
+ if (result.notified.length > 0) {
621
+ this.ctx.logger.debug(`assistant-work-items: recorded ${result.notified.length} Policy-change notice(s)`);
622
+ }
623
+ return;
624
+ }
625
+ after = result.next;
626
+ }
627
+ }
557
628
  /**
558
629
  * WP21 slice 6: on the same native wake, bind at most `maxPerTick`
559
630
  * dispatched items to the acceptance contract their own worker run froze.
@@ -573,12 +644,13 @@ export class AssistantWorkItemsService extends Service {
573
644
  if (bind === undefined || worker === null || this.workItemsClosed || this.workItemsVerifier() === undefined
574
645
  || typeof automations?.inspectOccurrenceAcceptance !== 'function')
575
646
  return;
647
+ const continuation = this.workItemsConfig.ownerContinuation?.automationId;
576
648
  const limit = bind.maxPerTick;
577
649
  const skips = this.workItemsBindWorkerRunSkips;
578
650
  const seen = new Set();
579
651
  let processed = 0, scanned = 0, wrapped = this.workItemsBindWorkerRunCursor === null, done = false;
580
652
  while (!done && processed < limit && scanned < WORKER_RUN_BIND_SCAN_LIMIT && !this.workItemsClosed) {
581
- const page = components.store.listLateAcceptanceCandidates(worker, this.workItemsBindWorkerRunCursor, 10);
653
+ const page = components.store.listLateAcceptanceCandidates(worker, this.workItemsBindWorkerRunCursor, 10, continuation === worker ? undefined : continuation);
582
654
  if (page.length === 0) {
583
655
  if (wrapped)
584
656
  break;
@@ -792,86 +864,63 @@ export class AssistantWorkItemsService extends Service {
792
864
  /**
793
865
  * WP21 slice 8 (`acceptance.holdReport`): the mandate binding, or else the
794
866
  * latest late binding while it still belongs to the item's current
795
- * occurrence. A malformed late row throws `storage`; callers decide whether
796
- * to hold (fail closed) or treat the item as unbound.
867
+ * occurrence. A malformed late row throws `storage` and cannot release a report.
797
868
  */
798
869
  workItemsAcceptanceBinding(components, item) {
799
870
  const mandate = item.authority?.mandate?.acceptance;
800
- if (mandate !== undefined)
871
+ if (mandate !== undefined) {
872
+ if (item.attempt > 1 && !this.workItemsRetryContractMatches(item, mandate))
873
+ return undefined;
801
874
  return mandate;
875
+ }
802
876
  const late = components.store.lateAcceptanceBinding(item.id);
803
877
  return late !== undefined && late.occurrenceId === item.occurrenceId ? late.binding : undefined;
804
878
  }
805
- /**
806
- * Whether this report must be held instead of dispatched: `holdReport` is
807
- * on, it is the terminal `ready` self-report of an executed item, the status
808
- * is notifiable, and the item already has an independent acceptance binding.
809
- * An item that has not been bound yet (for example a run that reports before
810
- * the wake that binds it) is reported normally, exactly as today.
811
- *
812
- * A verdict recorded for an earlier generation can never settle this report
813
- * (the store keeps at most one verdict per item and excludes settled items
814
- * from auto-settle), so holding it would just wait out `holdTimeoutMs` before
815
- * releasing it unverified. It is dispatched now, exactly like flag-off.
816
- * An unreadable verdict row fails closed and holds, like a malformed binding.
817
- */
879
+ /** An admission-time contract does not carry over to a new worker occurrence. */
880
+ workItemsRetryContractMatches(item, binding) {
881
+ return item.occurrenceId !== null && binding.taskKind === 'automation-run'
882
+ && binding.taskDigest === acceptanceDigest({ kind: 'automation-run', ref: workerRunTaskRef(item.occurrenceId) });
883
+ }
884
+ /** Hold the worker's terminal self-report even when its contract is not visible yet. */
818
885
  workItemsHoldsReport(components, item, report) {
819
- if (this.workItemsConfig.acceptanceHoldReport === undefined || this.workItemsClosed
820
- || report.status !== 'ready' || item.state !== 'ready' || item.observed)
821
- return false;
822
- if (!components.dispatch.shouldNotify(report))
823
- return false;
824
- if (this.workItemsAcceptanceBinding(components, item) === undefined)
886
+ return !this.workItemsClosed && item.attempt >= 1 && item.state === 'ready' && !item.observed
887
+ && report.status === 'ready' && report.deliveryState === null
888
+ && (this.workItemsConfig.acceptanceHoldReport !== undefined || this.workItemsContinuationRequiresAcceptance(components, item));
889
+ }
890
+ /** Read the requirement frozen at admission, including after live configuration changes. */
891
+ workItemsContinuationRequiresAcceptance(components, item) {
892
+ if (item.kind !== 'owner-continuation')
825
893
  return false;
826
- let verdict;
827
894
  try {
828
- verdict = components.store.acceptanceVerdict(item.id);
895
+ const record = components.store.getContinuation(item.id);
896
+ if (record === undefined)
897
+ return true;
898
+ return parseStoredContinuationPlan(record.planJson).contract.acceptance !== undefined;
829
899
  }
830
- catch {
900
+ catch (error) {
901
+ this.ctx.logger.warn(`assistant-work-items: continuation acceptance plan for ${item.id} is unreadable: ${describe(error)}`);
831
902
  return true;
832
903
  }
833
- return verdict === undefined || (verdict.attempt === item.attempt && verdict.occurrenceId === item.occurrenceId);
834
904
  }
835
- /**
836
- * Whether this `ready` report is still held. Used to imply the verdict gate
837
- * for held items even when `acceptance.verdictGate` itself is off: the gate
838
- * must move the item before the held report can be replaced by the blocked
839
- * one. A malformed binding row fails closed (treated as bound).
840
- */
905
+ /** The held report implies the verdict gate for this executed item. */
841
906
  workItemsReportIsHeld(components, item) {
842
- if (this.workItemsConfig.acceptanceHoldReport === undefined || item.state !== 'ready' || item.observed)
907
+ if (this.workItemsClosed || item.observed
908
+ || (this.workItemsConfig.acceptanceHoldReport === undefined && item.kind !== 'owner-continuation'))
843
909
  return false;
844
910
  const report = components.store.lastReport(item.id);
845
- if (report === undefined || report.status !== 'ready' || report.deliveryState !== null)
846
- return false;
847
- try {
848
- return this.workItemsAcceptanceBinding(components, item) !== undefined;
849
- }
850
- catch {
851
- return true;
852
- }
911
+ return report !== undefined && this.workItemsHoldsReport(components, item, report);
853
912
  }
854
913
  /**
855
- * WP21 slice 8: on the same native wake, release at most
856
- * `HELD_REPORT_RELEASE_LIMIT` held reports through an in-memory
857
- * `(recordedAt, itemId)` round-robin cursor. A full page advances the cursor
858
- * past rows that are still waiting, so they cannot starve a releasable row
859
- * behind them; a short page resets it and the next wake restarts from the
860
- * oldest row. Achievement sends the ordinary report; any other settled
861
- * verdict (or a verdict-gate `blocked` item) sends a blocked report instead;
862
- * a still-pending item stays held, and one whose `holdTimeoutMs` elapsed is
863
- * sent marked unverified. A row with no acceptance binding any more was never
864
- * really held and is recovered as an ordinary report instead of stranded.
865
- * One unreadable row (a malformed binding or verdict event) is warned about
866
- * and skipped without aborting the remaining releases; the fail-closed hold
867
- * itself is unchanged.
914
+ * Revisit undelivered terminal reports after binding and settlement on the
915
+ * same scheduler wake. The bounded cursor lets waiting rows share the scan
916
+ * with reports ready to release, including after a restart.
868
917
  */
869
918
  workItemsReleaseHeldReports(components) {
870
919
  const hold = this.workItemsConfig.acceptanceHoldReport;
871
- if (hold === undefined || this.workItemsClosed)
920
+ if (this.workItemsClosed)
872
921
  return;
873
922
  const now = Date.now();
874
- const page = components.store.listHeldAcceptanceReports(HELD_REPORT_RELEASE_LIMIT, this.workItemsHeldReportCursor ?? undefined);
923
+ const page = components.store.listHeldAcceptanceReports(HELD_REPORT_RELEASE_LIMIT, this.workItemsHeldReportCursor ?? undefined, hold === undefined);
875
924
  // Only a full page proves there may be rows this wake did not examine; a
876
925
  // short page ends the rotation so the next wake starts from the oldest.
877
926
  if (page.length < HELD_REPORT_RELEASE_LIMIT)
@@ -885,31 +934,21 @@ export class AssistantWorkItemsService extends Service {
885
934
  break;
886
935
  let release;
887
936
  try {
888
- let bound;
889
- try {
890
- bound = this.workItemsAcceptanceBinding(components, item) !== undefined;
891
- }
892
- catch (error) {
893
- if (!this.workItemsWarnMalformedLateBinding(item.id, error)) {
894
- this.ctx.logger.debug(`assistant-work-items: held report of ${item.id} cannot read its acceptance binding: ${describe(error)}`);
895
- }
896
- bound = true;
937
+ if (hold !== undefined)
938
+ release = this.workItemsHeldRelease(components, item);
939
+ else if (this.workItemsContinuationRequiresAcceptance(components, item)) {
940
+ release = { kind: 'blocked', reason: 'acceptance-disabled' };
897
941
  }
898
- // No binding any more means the row was never actually held (the report
899
- // committed before its pending delivery write, Delivery was missing,
900
- // `notifyOn` excluded `ready`, or a re-arm lost the binding). Recovery
901
- // sends the ordinary report instead of leaving it undelivered forever.
902
- release = bound
903
- ? this.workItemsHeldRelease(components, item, report, now, hold.holdTimeoutMs)
904
- : Object.freeze({ kind: 'plain' });
905
942
  }
906
943
  catch (error) {
907
- // One poison row (a malformed binding or verdict event) must not abort
908
- // the scan: warn and keep releasing the remaining rows.
909
944
  if (!this.workItemsWarnMalformedLateBinding(item.id, error)) {
910
945
  this.ctx.logger.warn(`assistant-work-items: held report of ${item.id} cannot be decided: ${describe(error)}`);
911
946
  }
912
- continue;
947
+ }
948
+ // A missing, stale or unreadable binding/verdict proves no achievement.
949
+ // The same deadline applies whether or not independent checking began.
950
+ if (release === undefined && hold !== undefined && now >= report.recordedAt + hold.holdTimeoutMs) {
951
+ release = { kind: 'unverified', timeoutMs: hold.holdTimeoutMs };
913
952
  }
914
953
  if (release === undefined)
915
954
  continue;
@@ -924,23 +963,26 @@ export class AssistantWorkItemsService extends Service {
924
963
  });
925
964
  }
926
965
  }
927
- /** Decide one held report without writing: release it, or wait for a later wake. */
928
- workItemsHeldRelease(components, item, report, now, timeoutMs) {
929
- if (item.state === 'blocked') {
930
- // The store query only returns acceptance-gated blocked rows.
966
+ /** Release only a verdict for this execution and its still-current contract. */
967
+ workItemsHeldRelease(components, item) {
968
+ const required = this.workItemsContinuationRequiresAcceptance(components, item);
969
+ if (item.state === 'blocked' && (!required || item.stateReason === 'acceptance-needs-attention')) {
970
+ // Needs-attention has no verdict. A persisted negative verdict for a
971
+ // required task must pass the same freshness checks as an achievement.
931
972
  return { kind: 'blocked', reason: item.stateReason ?? 'acceptance-blocked' };
932
973
  }
974
+ const binding = this.workItemsAcceptanceBinding(components, item);
975
+ if (binding === undefined || !storedBasisHolds(item))
976
+ return undefined;
933
977
  const verdict = components.store.acceptanceVerdict(item.id);
934
- // Only the verdict of this executed generation decides this report; an
935
- // older generation's verdict cannot be moved by the gate either, so the
936
- // held report waits for this run's verdict or the timeout.
937
- if (verdict !== undefined && verdict.attempt === item.attempt && verdict.occurrenceId === item.occurrenceId) {
938
- return verdict.objectiveStatus === 'achieved'
939
- ? { kind: 'achieved' }
940
- : { kind: 'blocked', reason: `acceptance-${verdict.objectiveStatus}` };
941
- }
942
- // A released report is never held again; the owning revision records the send.
943
- return now >= report.recordedAt + timeoutMs ? { kind: 'unverified', timeoutMs } : undefined;
978
+ if (verdict === undefined || verdict.attempt !== item.attempt || verdict.occurrenceId !== item.occurrenceId
979
+ || verdict.contractId !== binding.contractId || verdict.contractDigest !== binding.contractDigest)
980
+ return undefined;
981
+ if (required && !hasFreshAcceptanceReceipt(this.workItemsVerifier(), verdict))
982
+ return undefined;
983
+ return verdict.objectiveStatus === 'achieved'
984
+ ? { kind: 'achieved' }
985
+ : { kind: 'blocked', reason: `acceptance-${verdict.objectiveStatus}` };
944
986
  }
945
987
  get enabled() {
946
988
  return this.workItemsConfig.enabled;
@@ -1202,6 +1244,9 @@ export class AssistantWorkItemsService extends Service {
1202
1244
  }
1203
1245
  if (binding === undefined)
1204
1246
  fail('invalid-state', 'work item has no independent acceptance binding');
1247
+ if (this.workItemsConfig.acceptanceHoldReport !== undefined && item.attempt > 1
1248
+ && !this.workItemsRetryContractMatches(item, binding))
1249
+ fail('invalid-state', 'acceptance contract is not for the current retry occurrence');
1205
1250
  // The stored mandate must still be intact for this exact item; checked
1206
1251
  // locally so a pull-based settle never writes Policy audit.
1207
1252
  if (!storedBasisHolds(item))
@@ -1215,7 +1260,7 @@ export class AssistantWorkItemsService extends Service {
1215
1260
  // item before the held report is replaced by the blocked one.
1216
1261
  const gate = this.workItemsConfig.acceptanceVerdictGate === true || this.workItemsReportIsHeld(components, item)
1217
1262
  ? Object.freeze({ attempt: item.attempt, occurrenceId: item.occurrenceId }) : undefined;
1218
- const result = settleAcceptance(components.store, this.workItemsVerifier(), item, binding, gate);
1263
+ const result = settleAcceptance(components.store, this.workItemsVerifier(), item, binding, gate, this.workItemsContinuationRequiresAcceptance(components, item));
1219
1264
  // WP21 slice 7: the verifier's needs-attention writes no verdict, but with
1220
1265
  // the gate on it still moves this executed generation to `blocked`.
1221
1266
  if (gate !== undefined && result.outcome === 'needs-attention'
@@ -1283,13 +1328,51 @@ export class AssistantWorkItemsService extends Service {
1283
1328
  if (sourceText !== undefined && !authenticatesIntakeText(sourceText, candidate.contentDigest)) {
1284
1329
  fail('invalid-input', 'intake source text must be at most 65,536 UTF-16 units and match the content digest');
1285
1330
  }
1331
+ // Producer extensions are validated before any ledger write; an over-limit
1332
+ // objective or malformed handle rejects the whole call instead of being
1333
+ // truncated or stripped.
1334
+ const objective = intakeObjectiveOf(input, sourceText);
1335
+ // The objective becomes the work-item brief; admission truncates to
1336
+ // admission.maxBriefBytes, so a longer objective is rejected here rather
1337
+ // than silently truncated later.
1338
+ if (objective !== undefined && Buffer.byteLength(objective.text, 'utf8') > Math.min(MAX_INTAKE_OBJECTIVE_BYTES, this.workItemsConfig.admission.maxBriefBytes)) {
1339
+ fail('invalid-input', 'intake objective exceeds the configured admission brief limit');
1340
+ }
1341
+ const acceptance = intakeAcceptanceOf(input);
1286
1342
  const result = this.workItemsAvailable().store.recordIntake(candidate);
1343
+ this.rememberIntakeHint(result.record.source, result.record.sourceEventId, objective, acceptance);
1287
1344
  // `sourceText` is the authenticated text the content digest covers; with
1288
1345
  // evaluation off it is ignored, with it on it is held in memory only.
1289
1346
  if (sourceText !== undefined)
1290
1347
  this.workItemsIntakeEvaluation?.observe(result.record, sourceText);
1291
1348
  return result;
1292
1349
  }
1350
+ intakeHintKey(source, sourceEventId) {
1351
+ return `${source}\u0000${sourceEventId}`;
1352
+ }
1353
+ /**
1354
+ * Remember producer-supplied objective/acceptance for at most
1355
+ * `MAX_INTAKE_HINTS` candidates. The text is deliberately memory-only:
1356
+ * restart loses it and auto admission fails closed to observe, rather than
1357
+ * opening a new plaintext ledger for owner messages.
1358
+ */
1359
+ rememberIntakeHint(source, sourceEventId, objective, acceptance) {
1360
+ if (objective === undefined && acceptance === undefined)
1361
+ return;
1362
+ const key = this.intakeHintKey(source, sourceEventId);
1363
+ const previous = this.workItemsIntakeHints.get(key);
1364
+ this.workItemsIntakeHints.delete(key);
1365
+ this.workItemsIntakeHints.set(key, Object.freeze({
1366
+ ...(objective === undefined ? (previous?.objective === undefined ? {} : { objective: previous.objective }) : { objective }),
1367
+ ...(acceptance === undefined ? (previous?.acceptance === undefined ? {} : { acceptance: previous.acceptance }) : { acceptance }),
1368
+ }));
1369
+ while (this.workItemsIntakeHints.size > AssistantWorkItemsService.MAX_INTAKE_HINTS) {
1370
+ const oldest = this.workItemsIntakeHints.keys().next().value;
1371
+ if (oldest === undefined)
1372
+ break;
1373
+ this.workItemsIntakeHints.delete(oldest);
1374
+ }
1375
+ }
1293
1376
  /**
1294
1377
  * Host-only: register the injectable WP20 inferred-evaluation supplier
1295
1378
  * (Policy snapshot + model call). Requires `intakeLedger.evaluation`.
@@ -1473,18 +1556,7 @@ export class AssistantWorkItemsService extends Service {
1473
1556
  try {
1474
1557
  // A held report is simply not dispatched this pass: the same scheduler
1475
1558
  // wake that settles the binding releases it in `workItemsReleaseHeldReports`.
1476
- let held = false;
1477
- try {
1478
- held = this.workItemsHoldsReport(components, result.item, result.report);
1479
- }
1480
- catch (error) {
1481
- // A malformed late binding fails closed: keep holding; the timeout
1482
- // still releases it unverified rather than dropping the report.
1483
- if (!this.workItemsWarnMalformedLateBinding(item.id, error)) {
1484
- this.ctx.logger.debug(`assistant-work-items: report hold for ${item.id} cannot read its acceptance binding: ${describe(error)}`);
1485
- }
1486
- held = this.workItemsConfig.acceptanceHoldReport !== undefined && result.report.status === 'ready';
1487
- }
1559
+ const held = this.workItemsHoldsReport(components, result.item, result.report);
1488
1560
  notified = held ? false : await components.dispatch.notify(result.item, result.report);
1489
1561
  }
1490
1562
  catch (error) {