@dsh-enhanced/assistant-work-items 0.0.0-stage → 0.1.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +685 -2
- package/cordis.patch.yml +11 -0
- package/lib/acceptance-binding.d.ts +86 -0
- package/lib/acceptance-binding.d.ts.map +1 -0
- package/lib/acceptance-binding.js +250 -0
- package/lib/acceptance-binding.js.map +1 -0
- package/lib/admission.d.ts +95 -0
- package/lib/admission.d.ts.map +1 -0
- package/lib/admission.js +642 -0
- package/lib/admission.js.map +1 -0
- package/lib/authority.d.ts +99 -0
- package/lib/authority.d.ts.map +1 -0
- package/lib/authority.js +233 -0
- package/lib/authority.js.map +1 -0
- package/lib/config.d.ts +365 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +672 -0
- package/lib/config.js.map +1 -0
- package/lib/continuation-contract.d.ts +126 -0
- package/lib/continuation-contract.d.ts.map +1 -0
- package/lib/continuation-contract.js +109 -0
- package/lib/continuation-contract.js.map +1 -0
- package/lib/continuation-record.d.ts +32 -0
- package/lib/continuation-record.d.ts.map +1 -0
- package/lib/continuation-record.js +57 -0
- package/lib/continuation-record.js.map +1 -0
- package/lib/continuation.d.ts +89 -0
- package/lib/continuation.d.ts.map +1 -0
- package/lib/continuation.js +607 -0
- package/lib/continuation.js.map +1 -0
- package/lib/diagnosis-contract.d.ts +95 -0
- package/lib/diagnosis-contract.d.ts.map +1 -0
- package/lib/diagnosis-contract.js +167 -0
- package/lib/diagnosis-contract.js.map +1 -0
- package/lib/diagnosis.d.ts +70 -0
- package/lib/diagnosis.d.ts.map +1 -0
- package/lib/diagnosis.js +466 -0
- package/lib/diagnosis.js.map +1 -0
- package/lib/dispatch.d.ts +100 -0
- package/lib/dispatch.d.ts.map +1 -0
- package/lib/dispatch.js +317 -0
- package/lib/dispatch.js.map +1 -0
- package/lib/engineering-context.d.ts +86 -0
- package/lib/engineering-context.d.ts.map +1 -0
- package/lib/engineering-context.js +263 -0
- package/lib/engineering-context.js.map +1 -0
- package/lib/evaluation.d.ts +125 -0
- package/lib/evaluation.d.ts.map +1 -0
- package/lib/evaluation.js +295 -0
- package/lib/evaluation.js.map +1 -0
- package/lib/event-follow.d.ts +93 -0
- package/lib/event-follow.d.ts.map +1 -0
- package/lib/event-follow.js +150 -0
- package/lib/event-follow.js.map +1 -0
- package/lib/feedback-targets.d.ts +91 -0
- package/lib/feedback-targets.d.ts.map +1 -0
- package/lib/feedback-targets.js +362 -0
- package/lib/feedback-targets.js.map +1 -0
- package/lib/index.d.ts +27 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +25 -0
- package/lib/index.js.map +1 -0
- package/lib/intake-admission.d.ts +49 -0
- package/lib/intake-admission.d.ts.map +1 -0
- package/lib/intake-admission.js +100 -0
- package/lib/intake-admission.js.map +1 -0
- package/lib/intake-evaluation-runtime.d.ts +74 -0
- package/lib/intake-evaluation-runtime.d.ts.map +1 -0
- package/lib/intake-evaluation-runtime.js +343 -0
- package/lib/intake-evaluation-runtime.js.map +1 -0
- package/lib/intake-evaluation.d.ts +180 -0
- package/lib/intake-evaluation.d.ts.map +1 -0
- package/lib/intake-evaluation.js +440 -0
- package/lib/intake-evaluation.js.map +1 -0
- package/lib/intake.d.ts +118 -0
- package/lib/intake.d.ts.map +1 -0
- package/lib/intake.js +137 -0
- package/lib/intake.js.map +1 -0
- package/lib/ledger.d.ts +86 -0
- package/lib/ledger.d.ts.map +1 -0
- package/lib/ledger.js +145 -0
- package/lib/ledger.js.map +1 -0
- package/lib/mandate.d.ts +45 -0
- package/lib/mandate.d.ts.map +1 -0
- package/lib/mandate.js +168 -0
- package/lib/mandate.js.map +1 -0
- package/lib/outcome.d.ts +137 -0
- package/lib/outcome.d.ts.map +1 -0
- package/lib/outcome.js +303 -0
- package/lib/outcome.js.map +1 -0
- package/lib/owner-access.d.ts +35 -0
- package/lib/owner-access.d.ts.map +1 -0
- package/lib/owner-access.js +152 -0
- package/lib/owner-access.js.map +1 -0
- package/lib/owner-continuation.d.ts +27 -0
- package/lib/owner-continuation.d.ts.map +1 -0
- package/lib/owner-continuation.js +131 -0
- package/lib/owner-continuation.js.map +1 -0
- package/lib/owner-recall.d.ts +85 -0
- package/lib/owner-recall.d.ts.map +1 -0
- package/lib/owner-recall.js +134 -0
- package/lib/owner-recall.js.map +1 -0
- package/lib/owner-replies.d.ts +44 -0
- package/lib/owner-replies.d.ts.map +1 -0
- package/lib/owner-replies.js +268 -0
- package/lib/owner-replies.js.map +1 -0
- package/lib/peers.d.ts +624 -0
- package/lib/peers.d.ts.map +1 -0
- package/lib/peers.js +10 -0
- package/lib/peers.js.map +1 -0
- package/lib/policy-notice.d.ts +31 -0
- package/lib/policy-notice.d.ts.map +1 -0
- package/lib/policy-notice.js +137 -0
- package/lib/policy-notice.js.map +1 -0
- package/lib/reply-contract.d.ts +67 -0
- package/lib/reply-contract.d.ts.map +1 -0
- package/lib/reply-contract.js +58 -0
- package/lib/reply-contract.js.map +1 -0
- package/lib/service.d.ts +632 -0
- package/lib/service.d.ts.map +1 -0
- package/lib/service.js +2447 -0
- package/lib/service.js.map +1 -0
- package/lib/settlement.d.ts +74 -0
- package/lib/settlement.d.ts.map +1 -0
- package/lib/settlement.js +233 -0
- package/lib/settlement.js.map +1 -0
- package/lib/signals.d.ts +176 -0
- package/lib/signals.d.ts.map +1 -0
- package/lib/signals.js +497 -0
- package/lib/signals.js.map +1 -0
- package/lib/store.d.ts +791 -0
- package/lib/store.d.ts.map +1 -0
- package/lib/store.js +3266 -0
- package/lib/store.js.map +1 -0
- package/lib/tools.d.ts +7 -0
- package/lib/tools.d.ts.map +1 -0
- package/lib/tools.js +443 -0
- package/lib/tools.js.map +1 -0
- package/lib/types.d.ts +831 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +29 -0
- package/lib/types.js.map +1 -0
- package/lib/version.d.ts +2 -0
- package/lib/version.d.ts.map +1 -0
- package/lib/version.js +2 -0
- package/lib/version.js.map +1 -0
- package/lib/workspace.d.ts +43 -0
- package/lib/workspace.d.ts.map +1 -0
- package/lib/workspace.js +137 -0
- package/lib/workspace.js.map +1 -0
- package/package.json +101 -3
package/lib/admission.js
ADDED
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { byteLength, isActive, truncateUtf8 } from "./config.js";
|
|
3
|
+
import { buildAuthorityBasis } from "./authority.js";
|
|
4
|
+
import { resolveAcceptanceBinding } from "./acceptance-binding.js";
|
|
5
|
+
import { buildMandateBasis, decisionRefusal, mandateDigest, normalizeExecuteDecision } from "./mandate.js";
|
|
6
|
+
import { OPEN_STATES, WorkItemError } from "./types.js";
|
|
7
|
+
import { continuationSourceKey } from './continuation-record.js';
|
|
8
|
+
const KIND = /^[a-z][a-z0-9-]{0,63}$/;
|
|
9
|
+
const EXTERNAL_ID = /^[\x20-\x7e]{1,256}$/;
|
|
10
|
+
const BINDING_ID = /^[A-Za-z0-9][A-Za-z0-9:._-]{0,255}$/;
|
|
11
|
+
const SIGNAL_SOURCE = /^[a-z][a-z0-9-]{0,63}$/;
|
|
12
|
+
const SIGNAL_REVISION = /^[\x20-\x7e]{1,128}$/;
|
|
13
|
+
const EVIDENCE_KEY = /^[A-Za-z][A-Za-z0-9._-]{0,63}$/;
|
|
14
|
+
const THREAD_REF = /^[\x20-\x7e]{1,1024}$/;
|
|
15
|
+
/** Hard cap on followed threads per item; keeps resolution bounded per sweep. */
|
|
16
|
+
const MAX_FOLLOWS = 8;
|
|
17
|
+
const HOUR_MS = 3_600_000;
|
|
18
|
+
/** An item is settled when no further claim can move it without a new signal. */
|
|
19
|
+
function isSettled(state) {
|
|
20
|
+
return !OPEN_STATES.includes(state);
|
|
21
|
+
}
|
|
22
|
+
function provenanceOf(value) {
|
|
23
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
24
|
+
fail('invalid-input', 'provenance must be an object');
|
|
25
|
+
}
|
|
26
|
+
const allowed = ['source', 'sourceVersion', 'signalDigest', 'observedAt', 'evidence'];
|
|
27
|
+
for (const key of Object.keys(value)) {
|
|
28
|
+
if (!allowed.includes(key))
|
|
29
|
+
fail('invalid-input', `provenance contains unknown field ${key}`);
|
|
30
|
+
}
|
|
31
|
+
const source = text(value.source, 'provenance.source', 64);
|
|
32
|
+
if (!SIGNAL_SOURCE.test(source))
|
|
33
|
+
fail('invalid-input', 'provenance.source has an invalid shape');
|
|
34
|
+
const sourceVersion = text(value.sourceVersion, 'provenance.sourceVersion', 128);
|
|
35
|
+
if (!SIGNAL_REVISION.test(sourceVersion))
|
|
36
|
+
fail('invalid-input', 'provenance.sourceVersion has an invalid shape');
|
|
37
|
+
const signalDigest = text(value.signalDigest, 'provenance.signalDigest', 128);
|
|
38
|
+
if (!SIGNAL_REVISION.test(signalDigest))
|
|
39
|
+
fail('invalid-input', 'provenance.signalDigest has an invalid shape');
|
|
40
|
+
if (typeof value.observedAt !== 'number' || !Number.isSafeInteger(value.observedAt) || value.observedAt <= 0) {
|
|
41
|
+
fail('invalid-input', 'provenance.observedAt must be a positive integer timestamp');
|
|
42
|
+
}
|
|
43
|
+
let evidence;
|
|
44
|
+
if (value.evidence !== undefined) {
|
|
45
|
+
if (value.evidence === null || typeof value.evidence !== 'object' || Array.isArray(value.evidence)) {
|
|
46
|
+
fail('invalid-input', 'provenance.evidence must be an object');
|
|
47
|
+
}
|
|
48
|
+
const entries = Object.entries(value.evidence);
|
|
49
|
+
if (entries.length > 8)
|
|
50
|
+
fail('invalid-input', 'provenance.evidence accepts at most 8 entries');
|
|
51
|
+
evidence = {};
|
|
52
|
+
for (const [key, entry] of entries) {
|
|
53
|
+
if (!EVIDENCE_KEY.test(key))
|
|
54
|
+
fail('invalid-input', `provenance.evidence key ${key} has an invalid shape`);
|
|
55
|
+
if (typeof entry === 'string') {
|
|
56
|
+
if (byteLength(entry) > 256)
|
|
57
|
+
fail('invalid-input', `provenance.evidence.${key} exceeds 256 bytes`);
|
|
58
|
+
evidence[key] = entry;
|
|
59
|
+
}
|
|
60
|
+
else if (typeof entry === 'boolean') {
|
|
61
|
+
evidence[key] = entry;
|
|
62
|
+
}
|
|
63
|
+
else if (typeof entry === 'number' && Number.isFinite(entry)) {
|
|
64
|
+
evidence[key] = entry;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
fail('invalid-input', `provenance.evidence.${key} must be a string, number or boolean`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return Object.freeze({
|
|
72
|
+
source,
|
|
73
|
+
sourceVersion,
|
|
74
|
+
signalDigest,
|
|
75
|
+
observedAt: value.observedAt,
|
|
76
|
+
...(evidence === undefined ? {} : { evidence: Object.freeze(evidence) }),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/** Validate and dedupe the followed-thread list of one admission input. */
|
|
80
|
+
function threadRefsOf(value) {
|
|
81
|
+
if (value === undefined)
|
|
82
|
+
return Object.freeze([]);
|
|
83
|
+
if (!Array.isArray(value))
|
|
84
|
+
fail('invalid-input', 'follows must be an array of thread references');
|
|
85
|
+
if (value.length > MAX_FOLLOWS)
|
|
86
|
+
fail('invalid-input', `follows accepts at most ${MAX_FOLLOWS} thread references`);
|
|
87
|
+
const seen = new Set();
|
|
88
|
+
const refs = [];
|
|
89
|
+
for (const entry of value) {
|
|
90
|
+
if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
91
|
+
fail('invalid-input', 'each follow must be a thread reference object');
|
|
92
|
+
}
|
|
93
|
+
const allowed = ['kind', 'ref'];
|
|
94
|
+
for (const key of Object.keys(entry)) {
|
|
95
|
+
const known = allowed.includes(key);
|
|
96
|
+
if (!known)
|
|
97
|
+
fail('invalid-input', `follow contains unknown field ${key}`);
|
|
98
|
+
}
|
|
99
|
+
const kind = text(entry.kind, 'follows.kind', 64);
|
|
100
|
+
if (!KIND.test(kind))
|
|
101
|
+
fail('invalid-input', 'follows.kind has an invalid shape');
|
|
102
|
+
const ref = text(entry.ref, 'follows.ref', 1024);
|
|
103
|
+
if (!THREAD_REF.test(ref))
|
|
104
|
+
fail('invalid-input', 'follows.ref has an invalid shape');
|
|
105
|
+
// Duplicate refs collapse: an item can follow the same thread only once, so
|
|
106
|
+
// a repeated ref must not consume another position or budget slot.
|
|
107
|
+
const key = `${kind}\u0000${ref}`;
|
|
108
|
+
if (seen.has(key))
|
|
109
|
+
continue;
|
|
110
|
+
seen.add(key);
|
|
111
|
+
refs.push(Object.freeze({ kind, ref }));
|
|
112
|
+
}
|
|
113
|
+
return Object.freeze(refs);
|
|
114
|
+
}
|
|
115
|
+
function fail(code, message, detail) {
|
|
116
|
+
throw new WorkItemError(code, message, detail);
|
|
117
|
+
}
|
|
118
|
+
function text(value, field, maxBytes) {
|
|
119
|
+
if (typeof value !== 'string')
|
|
120
|
+
fail('invalid-input', `${field} must be a string`);
|
|
121
|
+
const normalized = value.normalize('NFC').trim();
|
|
122
|
+
if (normalized.length === 0)
|
|
123
|
+
fail('invalid-input', `${field} must not be empty`);
|
|
124
|
+
if (byteLength(normalized) > maxBytes)
|
|
125
|
+
fail('invalid-input', `${field} exceeds ${maxBytes} bytes`);
|
|
126
|
+
return normalized;
|
|
127
|
+
}
|
|
128
|
+
/** Stable per-source dedup identity for one external work signal. */
|
|
129
|
+
export function dedupKeyFor(sourceId, kind, externalId) {
|
|
130
|
+
return createHash('sha256').update(`${sourceId}\u0000${kind}\u0000${externalId}`, 'utf8').digest('hex');
|
|
131
|
+
}
|
|
132
|
+
/** Dedup key of an execute decision: source and decision external id only, never the kind. */
|
|
133
|
+
export function decisionDedupKeyFor(sourceId, externalId) {
|
|
134
|
+
// `execute-decision:v1` cannot be an ordinary kind (KIND forbids `:`), so the
|
|
135
|
+
// two key domains never collide.
|
|
136
|
+
return dedupKeyFor(sourceId, 'execute-decision:v1', externalId);
|
|
137
|
+
}
|
|
138
|
+
export function dedupBucketFor(createdAt, windowMs) {
|
|
139
|
+
return Math.floor(createdAt / windowMs);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The only write path for external work signals.
|
|
143
|
+
*
|
|
144
|
+
* Gate order is fixed and cheap-first, and every rejection happens before any
|
|
145
|
+
* durable write: an invalid owner route, a duplicate, an unlisted kind, a
|
|
146
|
+
* cooldown, the hourly rate or the open-item cap all leave the ledger
|
|
147
|
+
* untouched. In `observe` mode the same pipeline still records the item as
|
|
148
|
+
* `dropped`/`observe-only` with `observed = 1`, and never calls automations or
|
|
149
|
+
* delivery.
|
|
150
|
+
*/
|
|
151
|
+
export class WorkItemAdmission {
|
|
152
|
+
#deps;
|
|
153
|
+
#config;
|
|
154
|
+
#now;
|
|
155
|
+
#newId;
|
|
156
|
+
constructor(dependencies) {
|
|
157
|
+
this.#deps = dependencies;
|
|
158
|
+
this.#config = dependencies.config;
|
|
159
|
+
this.#now = dependencies.now ?? (() => Date.now());
|
|
160
|
+
this.#newId = dependencies.newId ?? (() => `wi-${Date.now().toString(36)}-${randomUUID().replace(/-/g, '').slice(0, 12)}`);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Validate and (maybe) admit one signal.
|
|
164
|
+
*
|
|
165
|
+
* Returns `created: false` with the existing item when the same signal was
|
|
166
|
+
* already admitted inside the dedup window.
|
|
167
|
+
*/
|
|
168
|
+
admit(input) {
|
|
169
|
+
return this.#admit(input, undefined);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Admit one execute decision under a frozen mandate.
|
|
173
|
+
*
|
|
174
|
+
* Order: shape → active-only → owner route → decision replay (any state, any
|
|
175
|
+
* dedup window; a different mandate under the same id is `identity-mismatch`)
|
|
176
|
+
* → inferred gate (auto mode, named Policy rule, remaining budget ≥ 1) →
|
|
177
|
+
* ordinary kind/cooldown/rate/capacity gates → Policy `admit`, whose allowing
|
|
178
|
+
* rule must equal the named rule. A decision carrying an acceptance handle
|
|
179
|
+
* is also bound to the verifier's exact contract for this owner scope
|
|
180
|
+
* (after the inferred gate, before the ordinary gates). Every refusal
|
|
181
|
+
* happens before any durable write; the mandate is persisted inside the
|
|
182
|
+
* item's authority basis.
|
|
183
|
+
*/
|
|
184
|
+
admitDecision(value) {
|
|
185
|
+
const config = this.#config;
|
|
186
|
+
if (!config.enabled)
|
|
187
|
+
fail('disabled', 'the work-item intake is disabled');
|
|
188
|
+
const decision = normalizeExecuteDecision(value);
|
|
189
|
+
if (!isActive(config))
|
|
190
|
+
fail('observe-mode', 'execute decisions are admitted only in active mode');
|
|
191
|
+
const result = this.#admit(decision.input, decision);
|
|
192
|
+
if (typeof result === 'string')
|
|
193
|
+
return Object.freeze({ outcome: 'refused', reasonCode: result });
|
|
194
|
+
const mandate = result.item.authority?.mandate;
|
|
195
|
+
if (mandate === undefined)
|
|
196
|
+
fail('storage', 'decision item is missing its frozen mandate');
|
|
197
|
+
return Object.freeze({
|
|
198
|
+
outcome: result.created ? 'admitted' : 'replayed',
|
|
199
|
+
item: result.item,
|
|
200
|
+
dispatch: result.dispatch,
|
|
201
|
+
mandate,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
#admit(input, decision) {
|
|
205
|
+
const config = this.#config;
|
|
206
|
+
if (!config.enabled)
|
|
207
|
+
fail('disabled', 'the work-item intake is disabled');
|
|
208
|
+
const continuation = isActive(config) ? this.#deps.continuationFor?.(input) : undefined;
|
|
209
|
+
if (input.kind === 'owner-continuation' && !continuation)
|
|
210
|
+
fail('invalid-input', 'owner-continuation requires a private continuation mint');
|
|
211
|
+
const planBuilder = config.diagnosis && isActive(config) ? this.#deps.diagnosisPlanFor?.(input) : undefined;
|
|
212
|
+
if (continuation && planBuilder)
|
|
213
|
+
fail('invalid-input', 'continuation and diagnosis plans are exclusive');
|
|
214
|
+
if (config.diagnosis && isActive(config) && !planBuilder && !continuation)
|
|
215
|
+
fail('invalid-input', 'diagnosis worker admits only its private canonical source');
|
|
216
|
+
const now = this.#now();
|
|
217
|
+
const normalized = this.#normalize(input);
|
|
218
|
+
const follows = this.#followInputs(normalized.follows);
|
|
219
|
+
const receipt = this.#validateOwnerRoute(now);
|
|
220
|
+
const active = isActive(config);
|
|
221
|
+
if (continuation) {
|
|
222
|
+
if (continuation.sourceKey !== continuationSourceKey(continuation.parentItemId, continuation.sourceDigest))
|
|
223
|
+
fail('invalid-input', 'continuation source key differs');
|
|
224
|
+
const prior = this.#deps.store.findContinuationBySourceKey(continuation.sourceKey);
|
|
225
|
+
if (prior) {
|
|
226
|
+
const item = this.#deps.store.findItemById(prior.itemId), scope = config.scope;
|
|
227
|
+
if (!item || !scope || item.scope.principalId !== scope.principalId || item.scope.ownerRouteId !== scope.ownerRouteId
|
|
228
|
+
|| item.scope.workspace !== scope.workspace || item.scope.preset !== scope.preset || item.owner.principalRecordId !== receipt.principalRecordId
|
|
229
|
+
|| item.owner.principalVersion !== receipt.principalVersion || receipt.generation < item.owner.generation
|
|
230
|
+
|| receipt.generation === item.owner.generation && receipt.bindingVersion !== item.owner.bindingVersion)
|
|
231
|
+
fail('identity-mismatch', 'continuation replay owner differs');
|
|
232
|
+
const dispatch = this.#deps.store.getDispatch(item.id);
|
|
233
|
+
if (!dispatch)
|
|
234
|
+
fail('storage', 'continuation is missing its dispatch row');
|
|
235
|
+
return Object.freeze({ item, created: false, reason: 'duplicate', dispatch });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// Lazy recovery keeps the open-item count honest without a timer.
|
|
239
|
+
if (!config.diagnosis)
|
|
240
|
+
this.#deps.ledger.recover();
|
|
241
|
+
// A decision's identity is its id alone: the kind is part of the mandate
|
|
242
|
+
// (and its digest), not of the key, so the same id under another kind is an
|
|
243
|
+
// identity mismatch instead of a second item. The key domain is disjoint
|
|
244
|
+
// from ordinary signals, which cannot squat or withdraw a decision id.
|
|
245
|
+
const dedupKey = decision === undefined
|
|
246
|
+
? dedupKeyFor(config.intakeSourceId, normalized.kind, normalized.externalId)
|
|
247
|
+
: decisionDedupKeyFor(config.intakeSourceId, normalized.externalId);
|
|
248
|
+
const dedupBucket = dedupBucketFor(now, config.admission.dedupWindowMs);
|
|
249
|
+
let mandate;
|
|
250
|
+
if (decision !== undefined) {
|
|
251
|
+
const digest = mandateDigest(decision, normalized);
|
|
252
|
+
// A decision id names exactly one item for its whole life: an open item
|
|
253
|
+
// in an older window or a settled one both answer the replay; neither
|
|
254
|
+
// the dedup window nor settlement may open a second item.
|
|
255
|
+
const prior = this.#deps.store.findLatestItemByDedupKey(dedupKey);
|
|
256
|
+
if (prior !== undefined) {
|
|
257
|
+
if (prior.authority?.mandate?.digest !== digest) {
|
|
258
|
+
fail('identity-mismatch', 'decision id already admitted a different mandate');
|
|
259
|
+
}
|
|
260
|
+
const dispatch = this.#deps.store.getDispatch(prior.id);
|
|
261
|
+
if (dispatch === undefined)
|
|
262
|
+
fail('storage', 'admitted item is missing its dispatch row');
|
|
263
|
+
return Object.freeze({ item: prior, created: false, reason: 'duplicate', dispatch });
|
|
264
|
+
}
|
|
265
|
+
const refusal = decisionRefusal(decision);
|
|
266
|
+
if (refusal !== undefined)
|
|
267
|
+
return refusal;
|
|
268
|
+
let acceptance;
|
|
269
|
+
if (decision.acceptance !== undefined) {
|
|
270
|
+
// One contract settles one item: another decision cannot reuse it.
|
|
271
|
+
// (This decision's own replay already returned above.)
|
|
272
|
+
if (this.#deps.store.findItemByAcceptanceContract(decision.acceptance.contractId) !== undefined)
|
|
273
|
+
return 'acceptance-contract-bound';
|
|
274
|
+
const verifier = typeof this.#deps.verifier === 'function' ? this.#deps.verifier() : this.#deps.verifier;
|
|
275
|
+
const resolved = resolveAcceptanceBinding(verifier, decision.acceptance, receipt, now);
|
|
276
|
+
if (typeof resolved === 'string')
|
|
277
|
+
return resolved;
|
|
278
|
+
acceptance = resolved;
|
|
279
|
+
}
|
|
280
|
+
mandate = buildMandateBasis(decision, digest, acceptance);
|
|
281
|
+
}
|
|
282
|
+
const existing = this.#deps.store.findItemByDedup(dedupKey, dedupBucket);
|
|
283
|
+
if (existing !== undefined) {
|
|
284
|
+
const dispatch = this.#deps.store.getDispatch(existing.id);
|
|
285
|
+
if (dispatch === undefined)
|
|
286
|
+
fail('storage', 'admitted item is missing its dispatch row');
|
|
287
|
+
return Object.freeze({ item: existing, created: false, reason: 'duplicate', dispatch });
|
|
288
|
+
}
|
|
289
|
+
// The window is a freshness bound, not a settlement bound: once the same
|
|
290
|
+
// canonical signal has been settled (completed, blocked, failed, dropped),
|
|
291
|
+
// a later corrected or withdrawn revision must not revive it as new work.
|
|
292
|
+
const settled = this.#deps.store.findLatestItemByDedupKey(dedupKey);
|
|
293
|
+
if (settled !== undefined && isSettled(settled.state)) {
|
|
294
|
+
const dispatch = this.#deps.store.getDispatch(settled.id);
|
|
295
|
+
if (dispatch === undefined)
|
|
296
|
+
fail('storage', 'admitted item is missing its dispatch row');
|
|
297
|
+
return Object.freeze({ item: settled, created: false, reason: 'fenced', dispatch });
|
|
298
|
+
}
|
|
299
|
+
// Canonical rows only: from here to `admitItem` there is no await, so a
|
|
300
|
+
// source alias (an event `@head`) is pinned in the same synchronous section
|
|
301
|
+
// that persists it.
|
|
302
|
+
const admittedFollows = this.#validateFollows(follows);
|
|
303
|
+
const wouldReject = this.#gateReasons(normalized.kind, now);
|
|
304
|
+
let authority = null;
|
|
305
|
+
if (active) {
|
|
306
|
+
const rejection = wouldReject[0];
|
|
307
|
+
if (rejection !== undefined)
|
|
308
|
+
fail(rejectionCode(rejection), rejectionMessage(rejection));
|
|
309
|
+
const authorized = this.#authorize(active, receipt, normalized.kind, dedupKey, dedupBucket, now, mandate);
|
|
310
|
+
if (authorized === 'policy-rule-mismatch')
|
|
311
|
+
return authorized;
|
|
312
|
+
authority = authorized;
|
|
313
|
+
if (mandate?.ruleId !== undefined && authority?.ruleId !== mandate.ruleId)
|
|
314
|
+
return 'policy-rule-mismatch';
|
|
315
|
+
}
|
|
316
|
+
const id = this.#newId();
|
|
317
|
+
const directory = this.#deps.workspace.directoryFor(id);
|
|
318
|
+
const truncatedBrief = byteLength(normalized.brief) > config.admission.maxBriefBytes;
|
|
319
|
+
const brief = truncatedBrief ? truncateUtf8(normalized.brief, config.admission.maxBriefBytes).value : normalized.brief;
|
|
320
|
+
const owner = Object.freeze({
|
|
321
|
+
principalRecordId: receipt.principalRecordId,
|
|
322
|
+
principalVersion: receipt.principalVersion,
|
|
323
|
+
workspace: receipt.workspace,
|
|
324
|
+
agentPreset: receipt.agentPreset,
|
|
325
|
+
bindingVersion: receipt.bindingVersion,
|
|
326
|
+
generation: receipt.generation,
|
|
327
|
+
});
|
|
328
|
+
const scope = config.scope;
|
|
329
|
+
if (scope === null)
|
|
330
|
+
fail('owner-route-unavailable', 'active admission requires a configured owner scope');
|
|
331
|
+
const item = Object.freeze({
|
|
332
|
+
id,
|
|
333
|
+
dedupKey,
|
|
334
|
+
dedupBucket,
|
|
335
|
+
sourceId: config.intakeSourceId,
|
|
336
|
+
kind: normalized.kind,
|
|
337
|
+
externalId: normalized.externalId,
|
|
338
|
+
title: normalized.title,
|
|
339
|
+
brief,
|
|
340
|
+
briefTruncated: truncatedBrief,
|
|
341
|
+
originBindingId: normalized.originBindingId,
|
|
342
|
+
acceptanceRef: normalized.acceptanceRef,
|
|
343
|
+
scope,
|
|
344
|
+
owner,
|
|
345
|
+
workerAutomationId: config.workerAutomationId ?? '',
|
|
346
|
+
authority,
|
|
347
|
+
state: active ? 'queued' : 'dropped',
|
|
348
|
+
stateReason: active ? null : 'observe-only',
|
|
349
|
+
attempt: 0,
|
|
350
|
+
maxAttempts: config.limits.maxAttempts,
|
|
351
|
+
leaseUntil: null,
|
|
352
|
+
leaseRunId: null,
|
|
353
|
+
occurrenceId: null,
|
|
354
|
+
workDir: directory,
|
|
355
|
+
lastReportStatus: null,
|
|
356
|
+
observed: !active,
|
|
357
|
+
createdAt: now,
|
|
358
|
+
updatedAt: now,
|
|
359
|
+
});
|
|
360
|
+
const dispatch = Object.freeze({
|
|
361
|
+
itemId: id,
|
|
362
|
+
automationId: config.workerAutomationId ?? '',
|
|
363
|
+
eventId: id,
|
|
364
|
+
occurredAt: now,
|
|
365
|
+
generation: 0,
|
|
366
|
+
wakeAttempts: 0,
|
|
367
|
+
state: active ? 'pending' : 'failed',
|
|
368
|
+
occurrenceId: null,
|
|
369
|
+
reason: active ? null : 'observe-only',
|
|
370
|
+
createdAt: now,
|
|
371
|
+
updatedAt: now,
|
|
372
|
+
});
|
|
373
|
+
const detail = { kind: normalized.kind, dedupBucket };
|
|
374
|
+
if (normalized.provenance !== null)
|
|
375
|
+
detail['provenance'] = normalized.provenance;
|
|
376
|
+
if (!active)
|
|
377
|
+
detail['wouldReject'] = wouldReject;
|
|
378
|
+
if (mandate !== undefined) {
|
|
379
|
+
detail['mandate'] = {
|
|
380
|
+
decisionId: mandate.decisionId, origin: mandate.origin, digest: mandate.digest,
|
|
381
|
+
...(mandate.acceptance === undefined ? {} : {
|
|
382
|
+
acceptance: { verifier: mandate.acceptance.verifier, author: mandate.acceptance.author, contractId: mandate.acceptance.contractId, contractDigest: mandate.acceptance.contractDigest },
|
|
383
|
+
}),
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
const continuationRecord = continuation?.create(item);
|
|
387
|
+
if (continuation && !continuationRecord)
|
|
388
|
+
fail('invalid-input', 'continuation builder did not freeze a record');
|
|
389
|
+
if (continuationRecord && (continuationRecord.sourceKey !== continuation.sourceKey || continuationRecord.sourceDigest !== continuation.sourceDigest || continuationRecord.parentItemId !== continuation.parentItemId))
|
|
390
|
+
fail('invalid-input', 'continuation builder changed its source identity');
|
|
391
|
+
const persisted = this.#deps.store.admitItem(item, {
|
|
392
|
+
kind: active ? 'admitted' : 'observed',
|
|
393
|
+
actor: 'admission',
|
|
394
|
+
detail,
|
|
395
|
+
}, dispatch, admittedFollows, planBuilder?.(item), continuationRecord);
|
|
396
|
+
if (persisted.id !== item.id) {
|
|
397
|
+
const existingDispatch = this.#deps.store.getDispatch(persisted.id);
|
|
398
|
+
if (!existingDispatch)
|
|
399
|
+
fail('storage', 'continuation is missing its dispatch row');
|
|
400
|
+
return Object.freeze({ item: persisted, created: false, reason: 'duplicate', dispatch: existingDispatch });
|
|
401
|
+
}
|
|
402
|
+
return Object.freeze({ item, created: true, dispatch });
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Retire the open item a withdrawn signal already produced, if any.
|
|
406
|
+
*
|
|
407
|
+
* Withdrawal shares `admit()`'s normalization and dedup identity, so a
|
|
408
|
+
* `cancelled` revision of an execution resolves to the same item as its
|
|
409
|
+
* `failed` revision. Every open row of that key is retired, not just the
|
|
410
|
+
* newest: the dedup window may have rolled while an older item stayed open.
|
|
411
|
+
* It deliberately skips the owner-route re-anchor, Policy
|
|
412
|
+
* authorization, cooldown, kind/rate/capacity gates and ledger recovery:
|
|
413
|
+
* those bound *new* work, while withdrawal only reduces pending work. It
|
|
414
|
+
* cannot recall a report already handed to `assistant-delivery`; it retires
|
|
415
|
+
* open state and a still-pending wake-up, and the worker's next tool call
|
|
416
|
+
* observes the dropped item.
|
|
417
|
+
*/
|
|
418
|
+
withdraw(input) {
|
|
419
|
+
const config = this.#config;
|
|
420
|
+
if (!config.enabled)
|
|
421
|
+
fail('disabled', 'the work-item intake is disabled');
|
|
422
|
+
// Normalization is shared with admission, so a malformed withdrawal —
|
|
423
|
+
// including a malformed provenance shape — is rejected rather than guessed.
|
|
424
|
+
const normalized = this.#normalize(input);
|
|
425
|
+
const dedupKey = dedupKeyFor(config.intakeSourceId, normalized.kind, normalized.externalId);
|
|
426
|
+
// The dedup window is a freshness bound, so settle the whole dedup key
|
|
427
|
+
// across windows: a withdrawal may arrive long after the failure it
|
|
428
|
+
// cancels, and an older still-open row must not survive it.
|
|
429
|
+
const withdrawn = this.#deps.store.withdrawByDedupKey(dedupKey, {
|
|
430
|
+
sourceId: normalized.provenance?.source ?? config.intakeSourceId,
|
|
431
|
+
externalId: normalized.externalId,
|
|
432
|
+
observedAt: normalized.provenance?.observedAt ?? this.#now(),
|
|
433
|
+
});
|
|
434
|
+
return Object.freeze({ outcome: withdrawn.outcome, items: withdrawn.items });
|
|
435
|
+
}
|
|
436
|
+
/** Gates that would reject this kind right now; empty when it may proceed. */
|
|
437
|
+
#gateReasons(kind, now) {
|
|
438
|
+
const config = this.#config;
|
|
439
|
+
const reasons = [];
|
|
440
|
+
if (!config.admission.allowedKinds.includes(kind))
|
|
441
|
+
reasons.push('kind-rejected');
|
|
442
|
+
const lastSameKind = this.#deps.store.lastAdmitAt(kind);
|
|
443
|
+
if (lastSameKind !== null && now - lastSameKind < config.admission.cooldownMs)
|
|
444
|
+
reasons.push('cooldown');
|
|
445
|
+
if (this.#deps.store.countItemsSince(now - HOUR_MS) >= config.admission.maxAdmitsPerHour)
|
|
446
|
+
reasons.push('rate-limited');
|
|
447
|
+
if (this.#deps.store.countOpenItems() >= config.admission.maxOpenItems)
|
|
448
|
+
reasons.push('capacity');
|
|
449
|
+
return Object.freeze(reasons);
|
|
450
|
+
}
|
|
451
|
+
#authorize(active, receipt, kind, dedupKey, dedupBucket, now, mandate) {
|
|
452
|
+
if (!active)
|
|
453
|
+
return null;
|
|
454
|
+
const policy = this.#deps.policy;
|
|
455
|
+
if (policy === undefined)
|
|
456
|
+
fail('missing-dependency', 'assistantPolicy is required for active admission');
|
|
457
|
+
const scope = this.#config.scope;
|
|
458
|
+
if (scope === null)
|
|
459
|
+
fail('owner-route-unavailable', 'active admission requires a configured owner scope');
|
|
460
|
+
const sourceId = this.#config.intakeSourceId;
|
|
461
|
+
const automationId = this.#config.workerAutomationId ?? '';
|
|
462
|
+
const request = {
|
|
463
|
+
subject: {
|
|
464
|
+
kind: 'background',
|
|
465
|
+
id: sourceId,
|
|
466
|
+
workspace: receipt.workspace,
|
|
467
|
+
principal: receipt.principalId,
|
|
468
|
+
},
|
|
469
|
+
action: 'admit',
|
|
470
|
+
resource: { kind: 'automation', id: automationId },
|
|
471
|
+
context: { initiator: 'background' },
|
|
472
|
+
};
|
|
473
|
+
const idempotencyKey = `work-item-admit:${dedupKey}:${String(dedupBucket)}`;
|
|
474
|
+
// A named rule is checked with the side-effect-free `evaluate` first, so a
|
|
475
|
+
// decision another rule would allow is refused without consuming the
|
|
476
|
+
// rule's authorization budget or writing an audit row. A deny falls
|
|
477
|
+
// through to `authorize`, which audits it as before. Older Policy peers
|
|
478
|
+
// without `evaluate` keep the post-authorize check below in `#admit`.
|
|
479
|
+
if (mandate?.ruleId !== undefined && typeof policy.evaluate === 'function') {
|
|
480
|
+
const preview = policy.evaluate(request);
|
|
481
|
+
if (preview.effect === 'allow' && preview.ruleId !== mandate.ruleId)
|
|
482
|
+
return 'policy-rule-mismatch';
|
|
483
|
+
}
|
|
484
|
+
const decision = policy.authorize(request, { idempotencyKey });
|
|
485
|
+
if (decision.effect !== 'allow') {
|
|
486
|
+
fail('source-rejected', `policy denied admission (${decision.reasonCode})`, kind);
|
|
487
|
+
}
|
|
488
|
+
return buildAuthorityBasis({ request, decision, sourceId, automationId, idempotencyKey, checkedAt: now, ...(mandate === undefined ? {} : { mandate }) });
|
|
489
|
+
}
|
|
490
|
+
/** Re-anchor the configured owner scope against the live delivery route. */
|
|
491
|
+
#validateOwnerRoute(now) {
|
|
492
|
+
const scope = this.#config.scope;
|
|
493
|
+
if (scope === null)
|
|
494
|
+
fail('owner-route-unavailable', 'no owner scope is configured');
|
|
495
|
+
if (scope.expiresAt <= now)
|
|
496
|
+
fail('owner-route-expired', 'the configured owner scope expired');
|
|
497
|
+
const delivery = typeof this.#deps.delivery === 'function' ? this.#deps.delivery() : this.#deps.delivery;
|
|
498
|
+
if (delivery === undefined)
|
|
499
|
+
fail('owner-route-unavailable', 'assistantDelivery is required to validate the owner route');
|
|
500
|
+
let receipt;
|
|
501
|
+
try {
|
|
502
|
+
receipt = delivery.validateOwnerRoute({
|
|
503
|
+
authorityId: scope.ownerRouteId,
|
|
504
|
+
principalId: scope.principalId,
|
|
505
|
+
workspace: scope.workspace,
|
|
506
|
+
agentPreset: scope.preset,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
511
|
+
const code = error !== null && typeof error === 'object' && 'code' in error && error.code === 'missing-binding'
|
|
512
|
+
? 'owner-route-invalid'
|
|
513
|
+
: 'owner-route-unavailable';
|
|
514
|
+
fail(code, 'owner route validation failed', detail);
|
|
515
|
+
}
|
|
516
|
+
if (receipt.receiptVersion !== 2)
|
|
517
|
+
fail('owner-route-invalid', 'owner route receipt has an unsupported version');
|
|
518
|
+
if (receipt.authorityId !== scope.ownerRouteId || receipt.workspace !== scope.workspace
|
|
519
|
+
|| receipt.agentPreset !== scope.preset || receipt.principalId !== scope.principalId) {
|
|
520
|
+
fail('owner-route-invalid', 'owner route receipt does not match the configured scope');
|
|
521
|
+
}
|
|
522
|
+
if (receipt.principalRecordId === '' || !Number.isSafeInteger(receipt.principalVersion)) {
|
|
523
|
+
fail('owner-route-invalid', 'owner route receipt is missing its durable principal identity');
|
|
524
|
+
}
|
|
525
|
+
return receipt;
|
|
526
|
+
}
|
|
527
|
+
#normalize(input) {
|
|
528
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input))
|
|
529
|
+
fail('invalid-input', 'admission input must be an object');
|
|
530
|
+
const allowed = ['kind', 'externalId', 'title', 'brief', 'originBindingId', 'acceptanceRef', 'provenance', 'follows'];
|
|
531
|
+
for (const key of Object.keys(input)) {
|
|
532
|
+
if (!allowed.includes(key))
|
|
533
|
+
fail('invalid-input', `admission input contains unknown field ${key}`);
|
|
534
|
+
}
|
|
535
|
+
const kind = text(input.kind, 'kind', 64);
|
|
536
|
+
if (!KIND.test(kind))
|
|
537
|
+
fail('kind-rejected', 'kind has an invalid shape');
|
|
538
|
+
const externalId = text(input.externalId, 'externalId', 256);
|
|
539
|
+
if (!EXTERNAL_ID.test(externalId) || externalId.includes('\u0000'))
|
|
540
|
+
fail('invalid-input', 'externalId has an invalid shape');
|
|
541
|
+
const title = text(input.title, 'title', 512);
|
|
542
|
+
const brief = text(input.brief, 'brief', Math.min(this.#config.admission.maxBriefBytes * 4, 262_144));
|
|
543
|
+
const originBindingId = text(input.originBindingId, 'originBindingId', 256);
|
|
544
|
+
if (!BINDING_ID.test(originBindingId))
|
|
545
|
+
fail('invalid-input', 'originBindingId has an invalid shape');
|
|
546
|
+
const acceptanceRef = input.acceptanceRef === undefined ? null : text(input.acceptanceRef, 'acceptanceRef', 1_024);
|
|
547
|
+
const provenance = input.provenance === undefined ? null : provenanceOf(input.provenance);
|
|
548
|
+
return { kind, externalId, title, brief, originBindingId, acceptanceRef, provenance, follows: threadRefsOf(input.follows) };
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Resolve admitted follow refs to durable follow rows.
|
|
552
|
+
*
|
|
553
|
+
* The registration is read once at admission so the stored `source` and
|
|
554
|
+
* `source_generation` are immutable facts of that admission: a later change of
|
|
555
|
+
* the source's generation degrades the follow rather than rewriting it.
|
|
556
|
+
*/
|
|
557
|
+
#followInputs(follows) {
|
|
558
|
+
if (follows.length === 0)
|
|
559
|
+
return Object.freeze([]);
|
|
560
|
+
const resolved = follows.map(ref => {
|
|
561
|
+
const registration = this.#deps.threadSource?.(ref.kind);
|
|
562
|
+
if (registration === undefined) {
|
|
563
|
+
fail('invalid-input', `followed thread kind ${ref.kind} has no registered source`);
|
|
564
|
+
}
|
|
565
|
+
return Object.freeze({
|
|
566
|
+
kind: ref.kind,
|
|
567
|
+
ref: ref.ref,
|
|
568
|
+
source: registration.source,
|
|
569
|
+
sourceGeneration: registration.generation,
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
return Object.freeze(resolved);
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Run each source's optional admission `validate` for a new item. Called
|
|
576
|
+
* only after every replay/dedup/fence answer, so retrying an already admitted
|
|
577
|
+
* signal returns its duplicate even when the ref is no longer admissible
|
|
578
|
+
* (for example an event cursor that has since been passed).
|
|
579
|
+
*
|
|
580
|
+
* A source's optional `canonicalize` rewrites each ref first; `validate` and
|
|
581
|
+
* the stored row see only the canonical form, and refs that canonicalize to
|
|
582
|
+
* the same thread collapse like literal duplicates.
|
|
583
|
+
*/
|
|
584
|
+
#validateFollows(follows) {
|
|
585
|
+
const seen = new Set();
|
|
586
|
+
const admitted = [];
|
|
587
|
+
for (const follow of follows) {
|
|
588
|
+
const registration = this.#deps.threadSource?.(follow.kind);
|
|
589
|
+
if (registration === undefined || registration.source !== follow.source || registration.generation !== follow.sourceGeneration) {
|
|
590
|
+
fail('invalid-input', `followed thread kind ${follow.kind} has no registered source`);
|
|
591
|
+
}
|
|
592
|
+
let ref = follow.ref;
|
|
593
|
+
if (registration.canonicalize !== undefined) {
|
|
594
|
+
try {
|
|
595
|
+
ref = registration.canonicalize(follow.ref);
|
|
596
|
+
}
|
|
597
|
+
catch {
|
|
598
|
+
ref = undefined;
|
|
599
|
+
}
|
|
600
|
+
if (typeof ref !== 'string' || !THREAD_REF.test(ref)) {
|
|
601
|
+
fail('invalid-input', `followed thread ref is not admissible for source ${registration.source}`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (registration.validate !== undefined) {
|
|
605
|
+
let accepted = false;
|
|
606
|
+
try {
|
|
607
|
+
accepted = registration.validate(ref) === true;
|
|
608
|
+
}
|
|
609
|
+
catch {
|
|
610
|
+
accepted = false;
|
|
611
|
+
}
|
|
612
|
+
if (!accepted)
|
|
613
|
+
fail('invalid-input', `followed thread ref is not admissible for source ${registration.source}`);
|
|
614
|
+
}
|
|
615
|
+
const key = `${follow.kind}\u0000${ref}`;
|
|
616
|
+
if (seen.has(key))
|
|
617
|
+
continue;
|
|
618
|
+
seen.add(key);
|
|
619
|
+
admitted.push(ref === follow.ref ? follow : Object.freeze({ ...follow, ref }));
|
|
620
|
+
}
|
|
621
|
+
return Object.freeze(admitted);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
function rejectionCode(reason) {
|
|
625
|
+
switch (reason) {
|
|
626
|
+
case 'kind-rejected': return 'kind-rejected';
|
|
627
|
+
case 'cooldown': return 'cooldown';
|
|
628
|
+
case 'rate-limited': return 'rate-limited';
|
|
629
|
+
case 'capacity': return 'capacity';
|
|
630
|
+
default: return 'invalid-input';
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
function rejectionMessage(reason) {
|
|
634
|
+
switch (reason) {
|
|
635
|
+
case 'kind-rejected': return 'this work-item kind is not admitted';
|
|
636
|
+
case 'cooldown': return 'this work-item kind is inside its admission cooldown';
|
|
637
|
+
case 'rate-limited': return 'the hourly admission rate is exhausted';
|
|
638
|
+
case 'capacity': return 'the open work-item cap is reached';
|
|
639
|
+
default: return 'admission rejected the signal';
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
//# sourceMappingURL=admission.js.map
|