@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.
- package/README.md +17 -10
- package/lib/acceptance-binding.d.ts +3 -1
- package/lib/acceptance-binding.d.ts.map +1 -1
- package/lib/acceptance-binding.js +26 -1
- package/lib/acceptance-binding.js.map +1 -1
- package/lib/authority.d.ts +7 -0
- package/lib/authority.d.ts.map +1 -1
- package/lib/authority.js +28 -0
- package/lib/authority.js.map +1 -1
- package/lib/continuation-contract.d.ts +1 -1
- package/lib/continuation-contract.d.ts.map +1 -1
- package/lib/continuation-contract.js +5 -2
- package/lib/continuation-contract.js.map +1 -1
- package/lib/continuation.d.ts.map +1 -1
- package/lib/continuation.js +7 -2
- package/lib/continuation.js.map +1 -1
- package/lib/dispatch.d.ts +6 -16
- package/lib/dispatch.d.ts.map +1 -1
- package/lib/dispatch.js +14 -18
- package/lib/dispatch.js.map +1 -1
- package/lib/intake-admission.d.ts +28 -2
- package/lib/intake-admission.d.ts.map +1 -1
- package/lib/intake-admission.js +22 -4
- package/lib/intake-admission.js.map +1 -1
- package/lib/intake.d.ts +38 -0
- package/lib/intake.d.ts.map +1 -1
- package/lib/intake.js +55 -1
- package/lib/intake.js.map +1 -1
- package/lib/mandate.d.ts.map +1 -1
- package/lib/mandate.js +4 -0
- package/lib/mandate.js.map +1 -1
- package/lib/owner-replies.d.ts.map +1 -1
- package/lib/owner-replies.js +37 -1
- package/lib/owner-replies.js.map +1 -1
- package/lib/peers.d.ts +2 -0
- package/lib/peers.d.ts.map +1 -1
- package/lib/reply-contract.d.ts +1 -1
- package/lib/reply-contract.d.ts.map +1 -1
- package/lib/reply-contract.js +2 -2
- package/lib/reply-contract.js.map +1 -1
- package/lib/service.d.ts +29 -35
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +177 -105
- package/lib/service.js.map +1 -1
- package/lib/store.d.ts +6 -4
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +15 -8
- package/lib/store.js.map +1 -1
- package/lib/types.d.ts +3 -1
- package/lib/types.d.ts.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- 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 {
|
|
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
|
|
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
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
*
|
|
856
|
-
*
|
|
857
|
-
*
|
|
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 (
|
|
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
|
-
|
|
889
|
-
|
|
890
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
928
|
-
workItemsHeldRelease(components, item
|
|
929
|
-
|
|
930
|
-
|
|
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
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
if (
|
|
938
|
-
return
|
|
939
|
-
|
|
940
|
-
|
|
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
|
-
|
|
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) {
|