@sellable/mcp 0.1.793 → 0.1.794
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/dist/agent-tool-names.d.ts +2 -2
- package/dist/agent-tool-names.js +14 -0
- package/dist/scheduler-envelope.d.ts +190 -0
- package/dist/scheduler-envelope.js +412 -0
- package/dist/server.js +28 -2
- package/dist/tools/campaign-message-preparation.d.ts +25 -0
- package/dist/tools/campaign-message-preparation.js +37 -0
- package/dist/tools/campaign-processing.d.ts +74 -1
- package/dist/tools/campaign-processing.js +57 -1
- package/dist/tools/prompts.d.ts +15 -3
- package/dist/tools/prompts.js +25 -0
- package/dist/tools/refill-executors.d.ts +43 -70
- package/dist/tools/refill-executors.js +201 -273
- package/dist/tools/refill-sends.d.ts +3 -3
- package/dist/tools/refill-sends.js +1 -1
- package/dist/tools/refill-target-plan.js +1 -1
- package/dist/tools/refill-v3-advance-contract.d.ts +618 -0
- package/dist/tools/refill-v3-advance-contract.js +920 -0
- package/dist/tools/refill-v3-advance.d.ts +397 -0
- package/dist/tools/refill-v3-advance.js +75 -0
- package/dist/tools/refill-v3-attention-packet-contract.d.ts +608 -0
- package/dist/tools/refill-v3-attention-packet-contract.js +911 -0
- package/dist/tools/refill-v3-campaign-work.d.ts +324 -0
- package/dist/tools/refill-v3-campaign-work.js +1015 -0
- package/dist/tools/refill-v3-continue-contract.d.ts +433 -0
- package/dist/tools/refill-v3-continue-contract.js +112 -0
- package/dist/tools/refill-v3-continue.d.ts +501 -0
- package/dist/tools/refill-v3-continue.js +654 -0
- package/dist/tools/refill-v3-edit-selected-campaign-contract.d.ts +208 -0
- package/dist/tools/refill-v3-edit-selected-campaign-contract.js +351 -0
- package/dist/tools/refill-v3-edit-selected-campaign.d.ts +232 -0
- package/dist/tools/refill-v3-edit-selected-campaign.js +147 -0
- package/dist/tools/refill-v3-fill-ready-contract.d.ts +198 -0
- package/dist/tools/refill-v3-fill-ready-contract.js +324 -0
- package/dist/tools/refill-v3-fill-ready.d.ts +199 -0
- package/dist/tools/refill-v3-fill-ready.js +136 -0
- package/dist/tools/refill-v3-source-family-dispatch.d.ts +249 -0
- package/dist/tools/refill-v3-source-family-dispatch.js +514 -0
- package/dist/tools/refill-v3-waterfall-contract.d.ts +156 -0
- package/dist/tools/refill-v3-waterfall-contract.js +181 -0
- package/dist/tools/refill-v3-waterfall.d.ts +136 -0
- package/dist/tools/refill-v3-waterfall.js +161 -0
- package/dist/tools/refill-v3-world-state-contract.d.ts +666 -0
- package/dist/tools/refill-v3-world-state-contract.js +790 -0
- package/dist/tools/refill-v3-world-state.d.ts +592 -0
- package/dist/tools/refill-v3-world-state.js +137 -0
- package/dist/tools/registry.d.ts +1710 -3
- package/dist/tools/registry.js +19 -0
- package/package.json +1 -1
- package/skills/refill-sends/SKILL.md +89 -4
- package/skills/refill-sends-waterfall-order/SKILL.md +174 -0
- package/skills/refill-sends-work-campaign/SKILL.md +253 -0
- package/skills/refill-sends-workflow/SKILL.md +112 -5
- package/skills/refill-sends-workflow/core/flow.v3.json +209 -0
|
@@ -0,0 +1,1015 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refill V3 selected-campaign traversal RUNTIME (Phase 146.5 Plan 09 T1,
|
|
3
|
+
* RV3PRM-001).
|
|
4
|
+
*
|
|
5
|
+
* This is AI seam #2's OWNED RUNTIME HOME (`A09-AI-SEAM-RUNTIME-HOME`). The
|
|
6
|
+
* markdown subskill supplies the model's judgment; this module is the
|
|
7
|
+
* deterministic authority that stands between that judgment and every product
|
|
8
|
+
* effect. It:
|
|
9
|
+
*
|
|
10
|
+
* 1. decodes the model's output into AT MOST ONE action drawn from the shared
|
|
11
|
+
* `REFILL_V3_CAMPAIGN_ATTENTION_ACTIONS` constant — never a local list;
|
|
12
|
+
* 2. refuses connection-health evidence before any prompt work;
|
|
13
|
+
* 3. gates every action behind live-job absence with a BOUNDED wait;
|
|
14
|
+
* 4. enforces enrich-before-rubric structurally (incident 112k);
|
|
15
|
+
* 5. consumes the canonical diagnostic order VERBATIM so an enriched island
|
|
16
|
+
* can never truncate traversal;
|
|
17
|
+
* 6. loads exactly ONE matching active provider prompt for a new provider
|
|
18
|
+
* search and exactly ZERO on a drain;
|
|
19
|
+
* 7. falsifies an AI exhaustion claim through the shared exhaustion predicate;
|
|
20
|
+
* 8. dispatches EXACTLY ONE authority, validates its receipt, and forces the
|
|
21
|
+
* exact sender/date two-lane re-observation before the iteration ends.
|
|
22
|
+
*
|
|
23
|
+
* The AI seam NEVER mutates. Every effect crosses `dispatchAuthority`, which the
|
|
24
|
+
* caller binds to an existing owner, and a second mutation from the same
|
|
25
|
+
* evidence packet is a closed typed failure.
|
|
26
|
+
*
|
|
27
|
+
* Package constraints (`A02-NO-ZOD`): no app `src/**` import, no validation
|
|
28
|
+
* library, hand-rolled guards only.
|
|
29
|
+
*/
|
|
30
|
+
import { REFILL_V3_CAMPAIGN_ATTENTION_ACTIONS, REFILL_V3_CAMPAIGN_ATTENTION_RESULTS, REFILL_V3_DIAGNOSTIC_SAMPLE_CAP, REFILL_V3_EXHAUSTION_PREDICATE, REFILL_V3_LEAD_SOURCE_FAMILIES, } from "./refill-v3-attention-packet-contract.js";
|
|
31
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
32
|
+
// ONE shared AI-output failure taxonomy for BOTH seams
|
|
33
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
34
|
+
/**
|
|
35
|
+
* The single closed taxonomy for "the model's output cannot be trusted".
|
|
36
|
+
* `refill-v3-waterfall.ts` IMPORTS this constant rather than forking a second
|
|
37
|
+
* AI-output framework (`A09-AI-SEAM-RUNTIME-HOME`, disposition D7).
|
|
38
|
+
*
|
|
39
|
+
* Every member is a VALUE, never a throw: a recoverable model error must not
|
|
40
|
+
* take the row down.
|
|
41
|
+
*/
|
|
42
|
+
export const REFILL_V3_AI_OUTPUT_FAILURES = [
|
|
43
|
+
"malformed_output",
|
|
44
|
+
"zero_actions",
|
|
45
|
+
"multiple_actions",
|
|
46
|
+
"duplicate_action",
|
|
47
|
+
"unsupported_action",
|
|
48
|
+
"stale_evidence",
|
|
49
|
+
"model_timeout",
|
|
50
|
+
"delegated_tool_timeout",
|
|
51
|
+
"receipt_incomplete",
|
|
52
|
+
"second_mutation_forbidden",
|
|
53
|
+
];
|
|
54
|
+
/** Deterministic REFUSALS that are about the world, not the model's syntax. */
|
|
55
|
+
export const REFILL_V3_CAMPAIGN_WORK_REFUSALS = [
|
|
56
|
+
"foreign_scope",
|
|
57
|
+
"health_recheck_required",
|
|
58
|
+
"live_job_blocks_action",
|
|
59
|
+
"rubric_before_enrichment",
|
|
60
|
+
"rubric_before_viable_repair",
|
|
61
|
+
"provider_prompt_missing",
|
|
62
|
+
"provider_prompt_mismatch",
|
|
63
|
+
"provider_prompt_forbidden_on_drain",
|
|
64
|
+
"exhaustion_falsified",
|
|
65
|
+
"negative_set_replay",
|
|
66
|
+
"diagnostic_order_not_consumed",
|
|
67
|
+
"cohort_arithmetic_forbidden",
|
|
68
|
+
"template_meaning_requires_approval",
|
|
69
|
+
];
|
|
70
|
+
/**
|
|
71
|
+
* TRANSPORT MIRROR of the app-owned diagnostic scan-termination union
|
|
72
|
+
* (`src/lib/workflow-tables/campaign-cell-selectors.ts`). The published package
|
|
73
|
+
* may not import app source, so the mirror is declared here and DRIFT IS
|
|
74
|
+
* PINNED: `tests/mcp/refill-v3/campaign-work-validator.test.ts` asserts this
|
|
75
|
+
* tuple equals the app-owned union member-for-member.
|
|
76
|
+
*
|
|
77
|
+
* ONLY `frontier_exhausted` is exhaustion evidence (incident 112t).
|
|
78
|
+
*/
|
|
79
|
+
export const REFILL_V3_DIAGNOSTIC_TERMINATIONS = [
|
|
80
|
+
"frontier_exhausted",
|
|
81
|
+
"scan_budget_exhausted",
|
|
82
|
+
"limit_reached",
|
|
83
|
+
];
|
|
84
|
+
/** Connection-health reason codes Plan 10 must intercept BEFORE this seam. */
|
|
85
|
+
export const REFILL_V3_HEALTH_REASON_CODES = [
|
|
86
|
+
"paid_inmail_connection_health_recheck_required",
|
|
87
|
+
"missing_balance",
|
|
88
|
+
"sender_connection_connecting",
|
|
89
|
+
"sender_connection_credentials",
|
|
90
|
+
"sender_disconnected",
|
|
91
|
+
"sales_nav_disconnected",
|
|
92
|
+
];
|
|
93
|
+
/** The bounded source modes. A drain loads NO provider prompt. */
|
|
94
|
+
export const REFILL_V3_SOURCE_COMMAND_MODES = [
|
|
95
|
+
"new_provider_search",
|
|
96
|
+
"drain_imported",
|
|
97
|
+
"post_engager_continuation",
|
|
98
|
+
];
|
|
99
|
+
/**
|
|
100
|
+
* Cohort/batch keys the model may NEVER name. Sizing belongs to the existing
|
|
101
|
+
* `campaign-message-preparation` adaptive worker; a batch size in the model's
|
|
102
|
+
* command is a second selector by another name.
|
|
103
|
+
*/
|
|
104
|
+
export const REFILL_V3_FORBIDDEN_COMMAND_KEYS = [
|
|
105
|
+
"batchSize",
|
|
106
|
+
"cohortSize",
|
|
107
|
+
"limit",
|
|
108
|
+
"offset",
|
|
109
|
+
"rangeStart",
|
|
110
|
+
"rangeEnd",
|
|
111
|
+
"targetCount",
|
|
112
|
+
"retryCount",
|
|
113
|
+
"attempts",
|
|
114
|
+
"cursor",
|
|
115
|
+
];
|
|
116
|
+
/** The SEVEN reuse points a preparation delegation receipt must carry. */
|
|
117
|
+
export const REFILL_V3_PREPARATION_REUSE_POINTS = [
|
|
118
|
+
"frontier",
|
|
119
|
+
"remainingGapPassEvidence",
|
|
120
|
+
"serialBatchCap",
|
|
121
|
+
"totalRowBudget",
|
|
122
|
+
"replayKey",
|
|
123
|
+
"lowPassGuard",
|
|
124
|
+
"reclaimBehavior",
|
|
125
|
+
];
|
|
126
|
+
function camelize(value) {
|
|
127
|
+
return value.replace(/_(.)/g, (_match, char) => char.toUpperCase());
|
|
128
|
+
}
|
|
129
|
+
function aliasMap(tokens) {
|
|
130
|
+
const entries = tokens.map((token) => [camelize(token), token]);
|
|
131
|
+
return Object.fromEntries(entries);
|
|
132
|
+
}
|
|
133
|
+
const ACTION = aliasMap(REFILL_V3_CAMPAIGN_ATTENTION_ACTIONS);
|
|
134
|
+
const RESULT = aliasMap(REFILL_V3_CAMPAIGN_ATTENTION_RESULTS);
|
|
135
|
+
/** Actions that may only be chosen once the bounded sample is ENRICHED. */
|
|
136
|
+
export const REFILL_V3_ENRICHMENT_GATED_ACTIONS = [
|
|
137
|
+
ACTION.requestRubricTrial,
|
|
138
|
+
ACTION.broadenSourceFamily,
|
|
139
|
+
ACTION.recordSourceFrontierExhausted,
|
|
140
|
+
];
|
|
141
|
+
/** Actions that OUTRANK rubric relaxation and source broadening. */
|
|
142
|
+
export const REFILL_V3_REPAIR_FIRST_ACTIONS = [
|
|
143
|
+
ACTION.rerunErroredCells,
|
|
144
|
+
ACTION.repairCampaignConfig,
|
|
145
|
+
ACTION.enrichBoundedSample,
|
|
146
|
+
ACTION.regenerateStaleMessages,
|
|
147
|
+
ACTION.patchFilterCriteria,
|
|
148
|
+
ACTION.patchSourceCriteria,
|
|
149
|
+
];
|
|
150
|
+
/** Row-scoped actions whose exact cohort is owned by the diagnostic authority. */
|
|
151
|
+
export const REFILL_V3_ROW_SCOPED_ACTIONS = [
|
|
152
|
+
ACTION.rerunErroredCells,
|
|
153
|
+
ACTION.enrichBoundedSample,
|
|
154
|
+
ACTION.regenerateStaleMessages,
|
|
155
|
+
];
|
|
156
|
+
/** Actions that carry a bounded source command. */
|
|
157
|
+
export const REFILL_V3_SOURCE_ACTIONS = [
|
|
158
|
+
ACTION.broadenSourceFamily,
|
|
159
|
+
ACTION.patchSourceCriteria,
|
|
160
|
+
];
|
|
161
|
+
/** One deterministic result per chosen action. */
|
|
162
|
+
export const REFILL_V3_ACTION_RESULTS = Object.freeze({
|
|
163
|
+
[ACTION.rerunErroredCells]: RESULT.cellsRerun,
|
|
164
|
+
[ACTION.repairCampaignConfig]: RESULT.campaignConfigRepaired,
|
|
165
|
+
[ACTION.enrichBoundedSample]: RESULT.boundedSampleEnriched,
|
|
166
|
+
[ACTION.regenerateStaleMessages]: RESULT.staleMessagesRegenerated,
|
|
167
|
+
[ACTION.patchFilterCriteria]: RESULT.filterCriteriaPatched,
|
|
168
|
+
[ACTION.patchSourceCriteria]: RESULT.sourceCriteriaPatched,
|
|
169
|
+
[ACTION.broadenSourceFamily]: RESULT.sourceFamilyBroadened,
|
|
170
|
+
[ACTION.requestRubricTrial]: RESULT.rubricTrialRequested,
|
|
171
|
+
[ACTION.awaitActiveJob]: RESULT.activeJobAwaited,
|
|
172
|
+
[ACTION.recordSourceFrontierExhausted]: RESULT.sourceFrontierExhausted,
|
|
173
|
+
});
|
|
174
|
+
/**
|
|
175
|
+
* Posts-search sub-mechanics the MODEL must respect. They live here because the
|
|
176
|
+
* prompt and the decoder are the two surfaces the model touches; the source
|
|
177
|
+
* dispatcher IMPORTS them rather than restating them.
|
|
178
|
+
*/
|
|
179
|
+
export const REFILL_V3_POSTS_KEYWORD_BATCH_SIZE = 5;
|
|
180
|
+
export const REFILL_V3_POSTS_MAX_SELECTED = 5;
|
|
181
|
+
export const REFILL_V3_POSTS_ROUND_BUDGET = 3;
|
|
182
|
+
/** Finite wait guidance so a stuck job can never hold a campaign forever. */
|
|
183
|
+
export const REFILL_V3_ACTIVE_JOB_WAIT_MS = 30_000;
|
|
184
|
+
export function createRefillV3CampaignWorkTaskScope() {
|
|
185
|
+
return { mutatedEvidenceFingerprints: new Set() };
|
|
186
|
+
}
|
|
187
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
188
|
+
// Hand-rolled guards
|
|
189
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
190
|
+
function isRecord(value) {
|
|
191
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
192
|
+
}
|
|
193
|
+
function isNonEmptyString(value) {
|
|
194
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
195
|
+
}
|
|
196
|
+
function isOneOf(value, options) {
|
|
197
|
+
return (typeof value === "string" && options.includes(value));
|
|
198
|
+
}
|
|
199
|
+
function isStringArray(value) {
|
|
200
|
+
return Array.isArray(value) && value.every(isNonEmptyString);
|
|
201
|
+
}
|
|
202
|
+
function sameOrder(left, right) {
|
|
203
|
+
if (left.length !== right.length)
|
|
204
|
+
return false;
|
|
205
|
+
return left.every((value, index) => value === right[index]);
|
|
206
|
+
}
|
|
207
|
+
function malformed(detail) {
|
|
208
|
+
return { ok: false, failure: "malformed_output", detail };
|
|
209
|
+
}
|
|
210
|
+
function unsupported(detail) {
|
|
211
|
+
return { ok: false, failure: "unsupported_action", detail };
|
|
212
|
+
}
|
|
213
|
+
const OUTPUT_KEYS = new Set([
|
|
214
|
+
"scopeEcho",
|
|
215
|
+
"actions",
|
|
216
|
+
"consumedDiagnosticRowIds",
|
|
217
|
+
]);
|
|
218
|
+
const SCOPE_ECHO_KEYS = new Set([
|
|
219
|
+
"campaignId",
|
|
220
|
+
"tableId",
|
|
221
|
+
"campaignVersion",
|
|
222
|
+
"evidenceFingerprint",
|
|
223
|
+
]);
|
|
224
|
+
const ACTION_KEYS = new Set([
|
|
225
|
+
"action",
|
|
226
|
+
"rationale",
|
|
227
|
+
"rowIds",
|
|
228
|
+
"sourceCommand",
|
|
229
|
+
"candidateVerdicts",
|
|
230
|
+
"templateFingerprint",
|
|
231
|
+
"templateMeaningChanged",
|
|
232
|
+
"exhaustionEvidence",
|
|
233
|
+
"providerPromptRef",
|
|
234
|
+
]);
|
|
235
|
+
const SOURCE_COMMAND_KEYS = new Set([
|
|
236
|
+
"family",
|
|
237
|
+
"mode",
|
|
238
|
+
"provider",
|
|
239
|
+
"requestFingerprint",
|
|
240
|
+
"query",
|
|
241
|
+
"filterDelta",
|
|
242
|
+
"keywordBatch",
|
|
243
|
+
"selectedPostIds",
|
|
244
|
+
"postRationales",
|
|
245
|
+
]);
|
|
246
|
+
const EXHAUSTION_KEYS = new Set([
|
|
247
|
+
"supply",
|
|
248
|
+
"activeJob",
|
|
249
|
+
"repairableExistingRows",
|
|
250
|
+
"approvedUndispatchedWork",
|
|
251
|
+
"structurallyUnactionableRows",
|
|
252
|
+
"retryExhaustedRows",
|
|
253
|
+
"scanTermination",
|
|
254
|
+
"searchedNegativeSet",
|
|
255
|
+
"yieldEvidence",
|
|
256
|
+
]);
|
|
257
|
+
/**
|
|
258
|
+
* DEEP scan for cohort/retry bookkeeping anywhere in the model's raw output.
|
|
259
|
+
*
|
|
260
|
+
* It runs BEFORE decoding so a batch size buried inside a nested command can
|
|
261
|
+
* never reach a dependency. Sizing belongs to the existing adaptive worker and a
|
|
262
|
+
* model-supplied cap is a second selector by another name.
|
|
263
|
+
*/
|
|
264
|
+
export function findRefillV3ForbiddenCommandKey(value) {
|
|
265
|
+
const queue = [value];
|
|
266
|
+
for (let index = 0; index < queue.length; index += 1) {
|
|
267
|
+
const current = queue[index];
|
|
268
|
+
if (Array.isArray(current)) {
|
|
269
|
+
for (const entry of current)
|
|
270
|
+
queue.push(entry);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (!isRecord(current))
|
|
274
|
+
continue;
|
|
275
|
+
for (const key of Object.keys(current)) {
|
|
276
|
+
if (REFILL_V3_FORBIDDEN_COMMAND_KEYS.includes(key)) {
|
|
277
|
+
return key;
|
|
278
|
+
}
|
|
279
|
+
queue.push(current[key]);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
function decodeSourceCommand(value) {
|
|
285
|
+
if (!isRecord(value)) {
|
|
286
|
+
return { ok: false, result: malformed("sourceCommand must be an object.") };
|
|
287
|
+
}
|
|
288
|
+
const unknownKey = Object.keys(value).find((key) => !SOURCE_COMMAND_KEYS.has(key));
|
|
289
|
+
if (unknownKey) {
|
|
290
|
+
return {
|
|
291
|
+
ok: false,
|
|
292
|
+
result: malformed(`Unknown sourceCommand field: ${unknownKey}`),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
if (!isOneOf(value.family, REFILL_V3_LEAD_SOURCE_FAMILIES)) {
|
|
296
|
+
return {
|
|
297
|
+
ok: false,
|
|
298
|
+
result: unsupported(`Source family ${String(value.family)} is outside the closed table.`),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
if (!isOneOf(value.mode, REFILL_V3_SOURCE_COMMAND_MODES)) {
|
|
302
|
+
return {
|
|
303
|
+
ok: false,
|
|
304
|
+
result: unsupported(`Source mode ${String(value.mode)} is outside the closed set.`),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
if (!isNonEmptyString(value.requestFingerprint)) {
|
|
308
|
+
return {
|
|
309
|
+
ok: false,
|
|
310
|
+
result: malformed("sourceCommand needs a requestFingerprint."),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
if (!(value.provider === null || isNonEmptyString(value.provider))) {
|
|
314
|
+
return {
|
|
315
|
+
ok: false,
|
|
316
|
+
result: malformed("provider must be a string or null."),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
if (!(value.query === null || typeof value.query === "string")) {
|
|
320
|
+
return { ok: false, result: malformed("query must be a string or null.") };
|
|
321
|
+
}
|
|
322
|
+
if (!(value.filterDelta === null || isRecord(value.filterDelta))) {
|
|
323
|
+
return {
|
|
324
|
+
ok: false,
|
|
325
|
+
result: malformed("filterDelta must be an object or null."),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
let keywordBatch = null;
|
|
329
|
+
if (value.keywordBatch !== null && value.keywordBatch !== undefined) {
|
|
330
|
+
if (!isStringArray(value.keywordBatch)) {
|
|
331
|
+
return {
|
|
332
|
+
ok: false,
|
|
333
|
+
result: malformed("keywordBatch must be an array of keywords."),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
if (value.keywordBatch.length !== REFILL_V3_POSTS_KEYWORD_BATCH_SIZE) {
|
|
337
|
+
return {
|
|
338
|
+
ok: false,
|
|
339
|
+
result: unsupported(`A posts-search round carries exactly ${REFILL_V3_POSTS_KEYWORD_BATCH_SIZE} keywords; ${value.keywordBatch.length} were supplied.`),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
keywordBatch = [...value.keywordBatch];
|
|
343
|
+
}
|
|
344
|
+
let selectedPostIds = null;
|
|
345
|
+
if (value.selectedPostIds !== null && value.selectedPostIds !== undefined) {
|
|
346
|
+
if (!isStringArray(value.selectedPostIds)) {
|
|
347
|
+
return {
|
|
348
|
+
ok: false,
|
|
349
|
+
result: malformed("selectedPostIds must be an array of post ids."),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
if (value.selectedPostIds.length > REFILL_V3_POSTS_MAX_SELECTED) {
|
|
353
|
+
return {
|
|
354
|
+
ok: false,
|
|
355
|
+
result: unsupported(`At most ${REFILL_V3_POSTS_MAX_SELECTED} candidate posts per round; ${value.selectedPostIds.length} were selected.`),
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
selectedPostIds = [...value.selectedPostIds];
|
|
359
|
+
}
|
|
360
|
+
let postRationales = null;
|
|
361
|
+
if (value.postRationales !== null && value.postRationales !== undefined) {
|
|
362
|
+
if (!Array.isArray(value.postRationales)) {
|
|
363
|
+
return {
|
|
364
|
+
ok: false,
|
|
365
|
+
result: malformed("postRationales must be an array."),
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
const decoded = [];
|
|
369
|
+
for (const entry of value.postRationales) {
|
|
370
|
+
if (!isRecord(entry) ||
|
|
371
|
+
!isNonEmptyString(entry.postId) ||
|
|
372
|
+
!isNonEmptyString(entry.expectedYieldBasis)) {
|
|
373
|
+
return {
|
|
374
|
+
ok: false,
|
|
375
|
+
result: malformed("Every selected post needs a postId and an expectedYieldBasis."),
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
decoded.push({
|
|
379
|
+
postId: entry.postId,
|
|
380
|
+
expectedYieldBasis: entry.expectedYieldBasis,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
postRationales = decoded;
|
|
384
|
+
}
|
|
385
|
+
// A selected post with no recorded rationale is an unauditable pick.
|
|
386
|
+
if (selectedPostIds !== null) {
|
|
387
|
+
const explained = new Set((postRationales ?? []).map((entry) => entry.postId));
|
|
388
|
+
const unexplained = selectedPostIds.filter((postId) => !explained.has(postId));
|
|
389
|
+
if (unexplained.length > 0) {
|
|
390
|
+
return {
|
|
391
|
+
ok: false,
|
|
392
|
+
result: malformed(`Selected posts without a recorded selection rationale: ${unexplained.join(", ")}`),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
ok: true,
|
|
398
|
+
value: {
|
|
399
|
+
family: value.family,
|
|
400
|
+
mode: value.mode,
|
|
401
|
+
provider: value.provider,
|
|
402
|
+
requestFingerprint: value.requestFingerprint,
|
|
403
|
+
query: value.query,
|
|
404
|
+
filterDelta: value.filterDelta,
|
|
405
|
+
keywordBatch,
|
|
406
|
+
selectedPostIds,
|
|
407
|
+
postRationales,
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
function decodeExhaustionEvidence(value) {
|
|
412
|
+
if (!isRecord(value))
|
|
413
|
+
return null;
|
|
414
|
+
if (Object.keys(value).some((key) => !EXHAUSTION_KEYS.has(key)))
|
|
415
|
+
return null;
|
|
416
|
+
if (!isRecord(value.supply) ||
|
|
417
|
+
!isRecord(value.searchedNegativeSet) ||
|
|
418
|
+
typeof value.repairableExistingRows !== "number" ||
|
|
419
|
+
typeof value.approvedUndispatchedWork !== "number" ||
|
|
420
|
+
typeof value.structurallyUnactionableRows !== "number" ||
|
|
421
|
+
typeof value.retryExhaustedRows !== "number" ||
|
|
422
|
+
typeof value.scanTermination !== "string") {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
return value;
|
|
426
|
+
}
|
|
427
|
+
function decodeCandidateVerdicts(value) {
|
|
428
|
+
if (value === null || value === undefined)
|
|
429
|
+
return { ok: true, value: null };
|
|
430
|
+
if (!Array.isArray(value))
|
|
431
|
+
return { ok: false };
|
|
432
|
+
const decoded = [];
|
|
433
|
+
for (const entry of value) {
|
|
434
|
+
if (!isRecord(entry) ||
|
|
435
|
+
!isNonEmptyString(entry.candidateId) ||
|
|
436
|
+
!isNonEmptyString(entry.rubricBasis) ||
|
|
437
|
+
!(entry.verdict === "pass" || entry.verdict === "fail")) {
|
|
438
|
+
return { ok: false };
|
|
439
|
+
}
|
|
440
|
+
decoded.push({
|
|
441
|
+
candidateId: entry.candidateId,
|
|
442
|
+
verdict: entry.verdict,
|
|
443
|
+
rubricBasis: entry.rubricBasis,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
return { ok: true, value: decoded };
|
|
447
|
+
}
|
|
448
|
+
function decodeActionEntry(value) {
|
|
449
|
+
if (!isRecord(value)) {
|
|
450
|
+
return {
|
|
451
|
+
ok: false,
|
|
452
|
+
result: malformed("Every action entry must be an object, never prose."),
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
const unknownKey = Object.keys(value).find((key) => !ACTION_KEYS.has(key));
|
|
456
|
+
if (unknownKey) {
|
|
457
|
+
return {
|
|
458
|
+
ok: false,
|
|
459
|
+
result: malformed(`Unknown action field: ${unknownKey}`),
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
if (!isRefillV3CampaignAttentionAction(value.action)) {
|
|
463
|
+
return {
|
|
464
|
+
ok: false,
|
|
465
|
+
result: unsupported(`Action ${String(value.action)} is not in the closed vocabulary.`),
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
if (!isNonEmptyString(value.rationale)) {
|
|
469
|
+
return {
|
|
470
|
+
ok: false,
|
|
471
|
+
result: malformed("Every action needs a non-empty rationale."),
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
let rowIds = null;
|
|
475
|
+
if (value.rowIds !== null && value.rowIds !== undefined) {
|
|
476
|
+
if (!isStringArray(value.rowIds)) {
|
|
477
|
+
return {
|
|
478
|
+
ok: false,
|
|
479
|
+
result: malformed("rowIds must be an array of ids."),
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
rowIds = [...value.rowIds];
|
|
483
|
+
}
|
|
484
|
+
let sourceCommand = null;
|
|
485
|
+
if (value.sourceCommand !== null && value.sourceCommand !== undefined) {
|
|
486
|
+
const decoded = decodeSourceCommand(value.sourceCommand);
|
|
487
|
+
if (!decoded.ok)
|
|
488
|
+
return { ok: false, result: decoded.result };
|
|
489
|
+
sourceCommand = decoded.value;
|
|
490
|
+
}
|
|
491
|
+
const verdicts = decodeCandidateVerdicts(value.candidateVerdicts);
|
|
492
|
+
if (!verdicts.ok) {
|
|
493
|
+
return {
|
|
494
|
+
ok: false,
|
|
495
|
+
result: malformed("Every candidate verdict needs an identity, a pass/fail, and a rubric basis."),
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
if (!(value.templateFingerprint === null ||
|
|
499
|
+
value.templateFingerprint === undefined ||
|
|
500
|
+
isNonEmptyString(value.templateFingerprint))) {
|
|
501
|
+
return {
|
|
502
|
+
ok: false,
|
|
503
|
+
result: malformed("templateFingerprint must be a string or null."),
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
if (value.templateMeaningChanged !== undefined &&
|
|
507
|
+
typeof value.templateMeaningChanged !== "boolean") {
|
|
508
|
+
return {
|
|
509
|
+
ok: false,
|
|
510
|
+
result: malformed("templateMeaningChanged must be a boolean."),
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
let exhaustionEvidence = null;
|
|
514
|
+
if (value.exhaustionEvidence !== null &&
|
|
515
|
+
value.exhaustionEvidence !== undefined) {
|
|
516
|
+
exhaustionEvidence = decodeExhaustionEvidence(value.exhaustionEvidence);
|
|
517
|
+
if (exhaustionEvidence === null) {
|
|
518
|
+
return {
|
|
519
|
+
ok: false,
|
|
520
|
+
result: malformed("exhaustionEvidence does not match the shared shape."),
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
let providerPromptRef = null;
|
|
525
|
+
if (value.providerPromptRef !== null &&
|
|
526
|
+
value.providerPromptRef !== undefined) {
|
|
527
|
+
const ref = value.providerPromptRef;
|
|
528
|
+
if (!isRecord(ref) ||
|
|
529
|
+
!isNonEmptyString(ref.provider) ||
|
|
530
|
+
!isNonEmptyString(ref.campaignOfferId)) {
|
|
531
|
+
return {
|
|
532
|
+
ok: false,
|
|
533
|
+
result: malformed("providerPromptRef needs an exact provider and campaignOfferId."),
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
providerPromptRef = {
|
|
537
|
+
provider: ref.provider,
|
|
538
|
+
campaignOfferId: ref.campaignOfferId,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
return {
|
|
542
|
+
ok: true,
|
|
543
|
+
value: {
|
|
544
|
+
action: value.action,
|
|
545
|
+
rationale: value.rationale,
|
|
546
|
+
rowIds,
|
|
547
|
+
sourceCommand,
|
|
548
|
+
candidateVerdicts: verdicts.value,
|
|
549
|
+
templateFingerprint: typeof value.templateFingerprint === "string"
|
|
550
|
+
? value.templateFingerprint
|
|
551
|
+
: null,
|
|
552
|
+
templateMeaningChanged: value.templateMeaningChanged === true,
|
|
553
|
+
exhaustionEvidence,
|
|
554
|
+
providerPromptRef,
|
|
555
|
+
},
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* SOLE decoder for the traversal's model output.
|
|
560
|
+
*
|
|
561
|
+
* The model answers with an ARRAY so zero, multiple, and duplicate choices each
|
|
562
|
+
* get their OWN typed failure instead of being silently collapsed into "the
|
|
563
|
+
* first one" — which is how a model that hedged two actions gets read as having
|
|
564
|
+
* decided one.
|
|
565
|
+
*/
|
|
566
|
+
export function decodeRefillV3CampaignWorkOutput(value) {
|
|
567
|
+
if (!isRecord(value)) {
|
|
568
|
+
return malformed("The traversal output must be an object, never narration.");
|
|
569
|
+
}
|
|
570
|
+
const unknownKey = Object.keys(value).find((key) => !OUTPUT_KEYS.has(key));
|
|
571
|
+
if (unknownKey) {
|
|
572
|
+
return malformed(`Unknown output field: ${unknownKey}`);
|
|
573
|
+
}
|
|
574
|
+
const echo = value.scopeEcho;
|
|
575
|
+
if (!isRecord(echo)) {
|
|
576
|
+
return malformed("The output must echo the selected scope.");
|
|
577
|
+
}
|
|
578
|
+
if (Object.keys(echo).some((key) => !SCOPE_ECHO_KEYS.has(key))) {
|
|
579
|
+
return malformed("scopeEcho carries a field outside the closed set.");
|
|
580
|
+
}
|
|
581
|
+
if (!isNonEmptyString(echo.campaignId) ||
|
|
582
|
+
!isNonEmptyString(echo.tableId) ||
|
|
583
|
+
!Number.isInteger(echo.campaignVersion) ||
|
|
584
|
+
!isNonEmptyString(echo.evidenceFingerprint)) {
|
|
585
|
+
return malformed("scopeEcho is incomplete.");
|
|
586
|
+
}
|
|
587
|
+
if (!Array.isArray(value.actions)) {
|
|
588
|
+
return malformed("actions must be an array so arity is decidable.");
|
|
589
|
+
}
|
|
590
|
+
if (value.actions.length === 0) {
|
|
591
|
+
return {
|
|
592
|
+
ok: false,
|
|
593
|
+
failure: "zero_actions",
|
|
594
|
+
detail: "The model returned no action.",
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
if (value.actions.length > 1) {
|
|
598
|
+
const tokens = value.actions.map((entry) => isRecord(entry) ? String(entry.action) : "");
|
|
599
|
+
const distinct = new Set(tokens);
|
|
600
|
+
return distinct.size === 1
|
|
601
|
+
? {
|
|
602
|
+
ok: false,
|
|
603
|
+
failure: "duplicate_action",
|
|
604
|
+
detail: `The model repeated ${tokens[0]} ${tokens.length} times.`,
|
|
605
|
+
}
|
|
606
|
+
: {
|
|
607
|
+
ok: false,
|
|
608
|
+
failure: "multiple_actions",
|
|
609
|
+
detail: `The model returned ${distinct.size} distinct actions: ${[...distinct].join(", ")}`,
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
const decodedAction = decodeActionEntry(value.actions[0]);
|
|
613
|
+
if (!decodedAction.ok)
|
|
614
|
+
return decodedAction.result;
|
|
615
|
+
let consumed = null;
|
|
616
|
+
if (value.consumedDiagnosticRowIds !== null &&
|
|
617
|
+
value.consumedDiagnosticRowIds !== undefined) {
|
|
618
|
+
if (!isStringArray(value.consumedDiagnosticRowIds)) {
|
|
619
|
+
return malformed("consumedDiagnosticRowIds must be an array of row ids.");
|
|
620
|
+
}
|
|
621
|
+
consumed = [...value.consumedDiagnosticRowIds];
|
|
622
|
+
}
|
|
623
|
+
return {
|
|
624
|
+
ok: true,
|
|
625
|
+
value: {
|
|
626
|
+
scopeEcho: {
|
|
627
|
+
campaignId: echo.campaignId,
|
|
628
|
+
tableId: echo.tableId,
|
|
629
|
+
campaignVersion: echo.campaignVersion,
|
|
630
|
+
evidenceFingerprint: echo.evidenceFingerprint,
|
|
631
|
+
},
|
|
632
|
+
actions: [decodedAction.value],
|
|
633
|
+
consumedDiagnosticRowIds: consumed,
|
|
634
|
+
},
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
638
|
+
// The one traversal iteration
|
|
639
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
640
|
+
function refuse(callLog, failure, detail, result = null) {
|
|
641
|
+
return { ok: false, failure, detail, result, callLog };
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Run ONE selected-campaign attention iteration.
|
|
645
|
+
*
|
|
646
|
+
* Ordering is load-bearing and every early refusal makes ZERO dependency calls:
|
|
647
|
+
*
|
|
648
|
+
* health → cohort bookkeeping → decode → exact scope → task-local replay →
|
|
649
|
+
* bounded diagnose → live-job gate → canonical order → repair-first →
|
|
650
|
+
* enrich-before-rubric → template meaning → provider prompt → negative set →
|
|
651
|
+
* exhaustion falsification → ONE authority → receipt → ONE re-observation.
|
|
652
|
+
*/
|
|
653
|
+
export async function runRefillV3CampaignWork(input) {
|
|
654
|
+
const { packet, dependencies } = input;
|
|
655
|
+
const callLog = [];
|
|
656
|
+
// 1. Connection health is Plan 10's, never this seam's. It outranks every
|
|
657
|
+
// other branch, including a legitimate live-job wait: editing config to
|
|
658
|
+
// mask a disconnected sender is the defect this refusal exists to stop.
|
|
659
|
+
if ((input.healthEvidence ?? []).length > 0) {
|
|
660
|
+
return refuse(callLog, "health_recheck_required", `Connection-health evidence must be handled before campaign work: ${(input.healthEvidence ?? []).join(", ")}`, RESULT.healthRecheckRequired);
|
|
661
|
+
}
|
|
662
|
+
// 2. Cohort/retry bookkeeping anywhere in the raw output.
|
|
663
|
+
const forbiddenKey = findRefillV3ForbiddenCommandKey(input.output);
|
|
664
|
+
if (forbiddenKey) {
|
|
665
|
+
return refuse(callLog, "cohort_arithmetic_forbidden", `The model supplied ${forbiddenKey}; sizing and replay bookkeeping belong to the deterministic owners.`);
|
|
666
|
+
}
|
|
667
|
+
// 3. One closed action.
|
|
668
|
+
const decoded = decodeRefillV3CampaignWorkOutput(input.output);
|
|
669
|
+
if (!decoded.ok) {
|
|
670
|
+
return refuse(callLog, decoded.failure, decoded.detail);
|
|
671
|
+
}
|
|
672
|
+
const chosen = decoded.value.actions[0];
|
|
673
|
+
const echo = decoded.value.scopeEcho;
|
|
674
|
+
// 4. Exact scope, then drift. Foreign identity and stale identity are
|
|
675
|
+
// DIFFERENT facts and must never collapse into one reason.
|
|
676
|
+
if (echo.campaignId !== packet.scope.campaignId ||
|
|
677
|
+
echo.tableId !== packet.scope.tableId) {
|
|
678
|
+
return refuse(callLog, "foreign_scope", `The output names ${echo.campaignId}/${echo.tableId}, not the selected ${packet.scope.campaignId}/${packet.scope.tableId}.`);
|
|
679
|
+
}
|
|
680
|
+
if (echo.campaignVersion !== packet.scope.campaignVersion ||
|
|
681
|
+
echo.evidenceFingerprint !== packet.scope.evidenceFingerprint) {
|
|
682
|
+
return refuse(callLog, "stale_evidence", `The output was formed against version ${echo.campaignVersion}/${echo.evidenceFingerprint}, not ${packet.scope.campaignVersion}/${packet.scope.evidenceFingerprint}.`);
|
|
683
|
+
}
|
|
684
|
+
// 5. One evidence packet can authorize at most one mutation per task.
|
|
685
|
+
if (input.taskScope?.mutatedEvidenceFingerprints.has(packet.scope.evidenceFingerprint)) {
|
|
686
|
+
return refuse(callLog, "second_mutation_forbidden", `Evidence ${packet.scope.evidenceFingerprint} already authorized one mutation; re-observe before choosing again.`);
|
|
687
|
+
}
|
|
688
|
+
// 6. The bounded diagnostic read. Exactly one call, capped at the canonical
|
|
689
|
+
// sample size, scoped to the selected campaign and table.
|
|
690
|
+
const sample = await dependencies.loadDiagnosticSample({
|
|
691
|
+
workspaceId: packet.scope.workspaceId,
|
|
692
|
+
campaignId: packet.scope.campaignId,
|
|
693
|
+
tableId: packet.scope.tableId,
|
|
694
|
+
limit: REFILL_V3_DIAGNOSTIC_SAMPLE_CAP,
|
|
695
|
+
});
|
|
696
|
+
callLog.push({
|
|
697
|
+
step: "diagnose",
|
|
698
|
+
detail: `${packet.scope.campaignId}:${packet.scope.tableId}:limit=${REFILL_V3_DIAGNOSTIC_SAMPLE_CAP}`,
|
|
699
|
+
});
|
|
700
|
+
// 7. Live-job gate, re-checked at the DISPATCH boundary against the shared
|
|
701
|
+
// TABLE frontier so two senders enrolled on one table cannot both dispatch.
|
|
702
|
+
const packetLiveJob = packet.evidence.activeJob;
|
|
703
|
+
const dispatchBoundaryLiveJob = packetLiveJob === null
|
|
704
|
+
? await dependencies.readLiveTableJob({ tableId: packet.scope.tableId })
|
|
705
|
+
: null;
|
|
706
|
+
const liveJob = packetLiveJob ?? dispatchBoundaryLiveJob;
|
|
707
|
+
if (liveJob !== null) {
|
|
708
|
+
// A model must obey a live job that was already in its packet. A job first
|
|
709
|
+
// observed (or revived) at the dispatch boundary is newer deterministic
|
|
710
|
+
// evidence the model could not have predicted, so the authority converts
|
|
711
|
+
// the stale mutation proposal into the only safe transition: a bounded
|
|
712
|
+
// wait with zero dispatch.
|
|
713
|
+
const becameLiveAtDispatchBoundary = packetLiveJob === null && dispatchBoundaryLiveJob !== null;
|
|
714
|
+
if (chosen.action !== ACTION.awaitActiveJob &&
|
|
715
|
+
!becameLiveAtDispatchBoundary) {
|
|
716
|
+
return refuse(callLog, "live_job_blocks_action", `A live ${liveJob.kind} job (${liveJob.status}) holds this frontier; the only legal action is a bounded wait.`);
|
|
717
|
+
}
|
|
718
|
+
callLog.push({ step: "await_active_job", detail: liveJob.id });
|
|
719
|
+
const reobservation = await dependencies.reobserveExactSenderDates({
|
|
720
|
+
workspaceId: packet.scope.workspaceId,
|
|
721
|
+
senderId: packet.scope.senderId,
|
|
722
|
+
date: packet.scope.date,
|
|
723
|
+
});
|
|
724
|
+
callLog.push({
|
|
725
|
+
step: "reobserve",
|
|
726
|
+
detail: `${packet.scope.senderId}:${packet.scope.date}`,
|
|
727
|
+
});
|
|
728
|
+
return {
|
|
729
|
+
ok: true,
|
|
730
|
+
action: ACTION.awaitActiveJob,
|
|
731
|
+
result: RESULT.activeJobAwaited,
|
|
732
|
+
receipt: {
|
|
733
|
+
outcome: "no_effect",
|
|
734
|
+
result: RESULT.activeJobAwaited,
|
|
735
|
+
scopeEcho: scopeEchoOf(packet),
|
|
736
|
+
idempotencyKey: idempotencyKeyOf(input, ACTION.awaitActiveJob),
|
|
737
|
+
beforeFingerprint: null,
|
|
738
|
+
afterFingerprint: null,
|
|
739
|
+
changedFields: [],
|
|
740
|
+
effectKey: null,
|
|
741
|
+
rollback: null,
|
|
742
|
+
reobserveRequired: true,
|
|
743
|
+
preparationDelegation: null,
|
|
744
|
+
systemicError: false,
|
|
745
|
+
lowPass: false,
|
|
746
|
+
refusalReason: null,
|
|
747
|
+
},
|
|
748
|
+
reobservation,
|
|
749
|
+
wait: {
|
|
750
|
+
activeJob: liveJob,
|
|
751
|
+
waitMs: REFILL_V3_ACTIVE_JOB_WAIT_MS,
|
|
752
|
+
reobserveRequired: true,
|
|
753
|
+
resumeGuidance: `Re-observe this sender/date after ${REFILL_V3_ACTIVE_JOB_WAIT_MS}ms and choose again from fresh evidence. The wait is finite: a job that is still live on the next observation is a stuck job, not a reason to block.`,
|
|
754
|
+
},
|
|
755
|
+
nextCohortCap: null,
|
|
756
|
+
callLog,
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
// 8. The backend owns the canonical bounded cohort. A model may echo an
|
|
760
|
+
// already-issued order for backward compatibility, but it never needs a
|
|
761
|
+
// discovery/retry call merely to learn row ids. Any supplied prefix,
|
|
762
|
+
// offset, or reordering is still refused.
|
|
763
|
+
if (decoded.value.consumedDiagnosticRowIds !== null &&
|
|
764
|
+
!sameOrder(decoded.value.consumedDiagnosticRowIds, sample.orderedRowIds)) {
|
|
765
|
+
return refuse(callLog, "diagnostic_order_not_consumed", `The echoed order [${decoded.value.consumedDiagnosticRowIds.join(",")}] is not the canonical order [${sample.orderedRowIds.join(",")}].`);
|
|
766
|
+
}
|
|
767
|
+
// 9. A FAILED job is not active: it routes to bounded just-in-time inspection
|
|
768
|
+
// and then to ONE repair, which OUTRANKS rubric relaxation and broadening.
|
|
769
|
+
if (packet.evidence.failedJobRef !== null) {
|
|
770
|
+
if (sample.failureEvidence.length === 0) {
|
|
771
|
+
return refuse(callLog, "stale_evidence", `The packet names failed job ${packet.evidence.failedJobRef} but the bounded inspection returned no failure evidence.`);
|
|
772
|
+
}
|
|
773
|
+
if (!REFILL_V3_REPAIR_FIRST_ACTIONS.includes(chosen.action)) {
|
|
774
|
+
return refuse(callLog, "rubric_before_viable_repair", `An exact repair for failed job ${packet.evidence.failedJobRef} outranks ${chosen.action}.`);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
// 10. Enrich BEFORE any rubric judgement. A rubric verdict over an unenriched
|
|
778
|
+
// sample is a false negative because the fields it reads do not exist yet
|
|
779
|
+
// (incident 112k), so it can never justify relaxation, broadening, or
|
|
780
|
+
// exhaustion.
|
|
781
|
+
if (sample.unenrichedRowIds.length > 0 &&
|
|
782
|
+
REFILL_V3_ENRICHMENT_GATED_ACTIONS.includes(chosen.action)) {
|
|
783
|
+
return refuse(callLog, "rubric_before_enrichment", `${sample.unenrichedRowIds.length} sampled rows are unenriched; ${chosen.action} is invalid until the bounded sample is enriched.`);
|
|
784
|
+
}
|
|
785
|
+
// 11. Template/sequence MEANING exits to the existing approval boundary. It
|
|
786
|
+
// cannot be smuggled through repair, source work, or rubric relaxation.
|
|
787
|
+
if (chosen.templateMeaningChanged) {
|
|
788
|
+
return refuse(callLog, "template_meaning_requires_approval", "A template or sequence meaning change belongs to the separate approval boundary.", RESULT.noActionAvailable);
|
|
789
|
+
}
|
|
790
|
+
// 12. Row selection must stay inside the canonical bounded frontier.
|
|
791
|
+
const resolvedRowIds = resolveRowIds(chosen, sample);
|
|
792
|
+
if (resolvedRowIds !== null) {
|
|
793
|
+
const canonical = new Set(sample.orderedRowIds);
|
|
794
|
+
const outside = resolvedRowIds.filter((rowId) => !canonical.has(rowId));
|
|
795
|
+
if (outside.length > 0) {
|
|
796
|
+
return refuse(callLog, "diagnostic_order_not_consumed", `Rows outside the canonical bounded frontier: ${outside.join(", ")}`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
// 13. Searched-negative-set replay. Checked BEFORE any provider guidance load
|
|
800
|
+
// so an identical search costs zero provider work.
|
|
801
|
+
if (chosen.sourceCommand !== null &&
|
|
802
|
+
packet.evidence.searchedNegativeSetFingerprints.includes(chosen.sourceCommand.requestFingerprint)) {
|
|
803
|
+
return refuse(callLog, "negative_set_replay", `Request fingerprint ${chosen.sourceCommand.requestFingerprint} is already in the searched negative set for this campaign.`);
|
|
804
|
+
}
|
|
805
|
+
// 14. Exactly ONE matching active provider prompt for a new provider search or
|
|
806
|
+
// a prompt-package-bound exhaustion record; exactly ZERO on a drain.
|
|
807
|
+
const promptGate = resolveProviderPromptRequirement(chosen, packet);
|
|
808
|
+
if (promptGate.kind === "refuse") {
|
|
809
|
+
return refuse(callLog, promptGate.failure, promptGate.detail);
|
|
810
|
+
}
|
|
811
|
+
let providerPrompt = null;
|
|
812
|
+
if (promptGate.kind === "load") {
|
|
813
|
+
providerPrompt = await dependencies.loadProviderPrompt({
|
|
814
|
+
provider: promptGate.provider,
|
|
815
|
+
campaignOfferId: packet.scope.campaignId,
|
|
816
|
+
});
|
|
817
|
+
callLog.push({
|
|
818
|
+
step: "load_provider_prompt",
|
|
819
|
+
detail: `${promptGate.provider}:${packet.scope.campaignId}`,
|
|
820
|
+
});
|
|
821
|
+
if (providerPrompt === null) {
|
|
822
|
+
return refuse(callLog, "provider_prompt_missing", `${promptGate.provider} has no active campaign-scoped guidance for ${packet.scope.campaignId}; missing guidance can never license a search or prove exhaustion.`);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
// 15. Every exhaustion claim is falsified against the SHARED predicate. The
|
|
826
|
+
// AI never terminates a campaign on its own word.
|
|
827
|
+
let exhaustionVerdict = null;
|
|
828
|
+
if (chosen.action === ACTION.recordSourceFrontierExhausted) {
|
|
829
|
+
if (chosen.exhaustionEvidence === null) {
|
|
830
|
+
return refuse(callLog, "stale_evidence", "An exhaustion claim must carry the bounded evidence it was formed from.");
|
|
831
|
+
}
|
|
832
|
+
exhaustionVerdict = falsifyRefillV3ExhaustionClaim(chosen.exhaustionEvidence);
|
|
833
|
+
if (!exhaustionVerdict.exhausted) {
|
|
834
|
+
return refuse(callLog, "exhaustion_falsified", `Live evidence contradicts the exhaustion claim: ${exhaustionVerdict.falsifyingClause}. The flow continues.`);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
// 16. EXACTLY ONE authority dispatch. Duplicate-work identity keys to the
|
|
838
|
+
// TABLE frontier, never to the enrolled sender (incident 112ak).
|
|
839
|
+
const request = {
|
|
840
|
+
action: chosen.action,
|
|
841
|
+
scope: { ...packet.scope },
|
|
842
|
+
frontierKey: `${packet.scope.tableId}:${chosen.action}`,
|
|
843
|
+
idempotencyKey: idempotencyKeyOf(input, chosen.action),
|
|
844
|
+
rowIds: resolvedRowIds,
|
|
845
|
+
sourceCommand: chosen.sourceCommand,
|
|
846
|
+
candidateVerdicts: chosen.candidateVerdicts,
|
|
847
|
+
providerPrompt,
|
|
848
|
+
exhaustionVerdict,
|
|
849
|
+
};
|
|
850
|
+
const receipt = await dependencies.dispatchAuthority(request);
|
|
851
|
+
callLog.push({ step: "authority", detail: chosen.action });
|
|
852
|
+
const receiptCheck = validateReceipt(receipt, request, chosen);
|
|
853
|
+
if (receiptCheck !== null) {
|
|
854
|
+
return refuse(callLog, receiptCheck.failure, receiptCheck.detail);
|
|
855
|
+
}
|
|
856
|
+
// 17. Immediate exact sender/date two-lane re-observation, then STOP.
|
|
857
|
+
const reobservation = await dependencies.reobserveExactSenderDates({
|
|
858
|
+
workspaceId: packet.scope.workspaceId,
|
|
859
|
+
senderId: packet.scope.senderId,
|
|
860
|
+
date: packet.scope.date,
|
|
861
|
+
});
|
|
862
|
+
callLog.push({
|
|
863
|
+
step: "reobserve",
|
|
864
|
+
detail: `${packet.scope.senderId}:${packet.scope.date}`,
|
|
865
|
+
});
|
|
866
|
+
input.taskScope?.mutatedEvidenceFingerprints.add(packet.scope.evidenceFingerprint);
|
|
867
|
+
return {
|
|
868
|
+
ok: true,
|
|
869
|
+
action: chosen.action,
|
|
870
|
+
result: receipt.result,
|
|
871
|
+
receipt,
|
|
872
|
+
reobservation,
|
|
873
|
+
wait: null,
|
|
874
|
+
// The shrink-back rule is BOUND to the worker's own low-pass guard; this
|
|
875
|
+
// layer names the canonical bounded cap and computes no cohort of its own.
|
|
876
|
+
nextCohortCap: receipt.systemicError || receipt.lowPass
|
|
877
|
+
? REFILL_V3_DIAGNOSTIC_SAMPLE_CAP
|
|
878
|
+
: null,
|
|
879
|
+
callLog,
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
function scopeEchoOf(packet) {
|
|
883
|
+
return `${packet.scope.workspaceId}:${packet.scope.campaignId}:${packet.scope.tableId}`;
|
|
884
|
+
}
|
|
885
|
+
function idempotencyKeyOf(input, action) {
|
|
886
|
+
if (isNonEmptyString(input.idempotencyKey))
|
|
887
|
+
return input.idempotencyKey;
|
|
888
|
+
const { scope } = input.packet;
|
|
889
|
+
return `${scope.workspaceId}:${scope.campaignId}:${scope.tableId}:${scope.evidenceFingerprint}:${action}`;
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Every row-scoped action consumes the diagnostic authority's exact bounded
|
|
893
|
+
* cohort. The model chooses WHAT to do; it never chooses the batch.
|
|
894
|
+
*/
|
|
895
|
+
function resolveRowIds(chosen, sample) {
|
|
896
|
+
if (chosen.action === ACTION.enrichBoundedSample ||
|
|
897
|
+
chosen.action === ACTION.regenerateStaleMessages) {
|
|
898
|
+
return sample.orderedRowIds;
|
|
899
|
+
}
|
|
900
|
+
if (chosen.action === ACTION.rerunErroredCells)
|
|
901
|
+
return sample.failedRowIds;
|
|
902
|
+
return chosen.rowIds;
|
|
903
|
+
}
|
|
904
|
+
function resolveProviderPromptRequirement(chosen, packet) {
|
|
905
|
+
const needsGuidance = chosen.action === ACTION.recordSourceFrontierExhausted ||
|
|
906
|
+
(chosen.sourceCommand !== null &&
|
|
907
|
+
chosen.sourceCommand.mode === "new_provider_search");
|
|
908
|
+
if (!needsGuidance) {
|
|
909
|
+
// A drain of already-imported supply and a non-provider post-engager
|
|
910
|
+
// continuation require and mark NO provider guidance. Preloading one anyway
|
|
911
|
+
// contradicts the declared mode.
|
|
912
|
+
if (chosen.providerPromptRef !== null) {
|
|
913
|
+
return {
|
|
914
|
+
kind: "refuse",
|
|
915
|
+
failure: "provider_prompt_forbidden_on_drain",
|
|
916
|
+
detail: "This action performs no new provider search, so it must load zero provider prompts.",
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
return { kind: "skip" };
|
|
920
|
+
}
|
|
921
|
+
const ref = chosen.providerPromptRef;
|
|
922
|
+
if (ref === null) {
|
|
923
|
+
return {
|
|
924
|
+
kind: "refuse",
|
|
925
|
+
failure: "provider_prompt_mismatch",
|
|
926
|
+
detail: "A new provider search and an exhaustion record each require the matching active provider guidance to be named.",
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
if (ref.campaignOfferId !== packet.scope.campaignId) {
|
|
930
|
+
return {
|
|
931
|
+
kind: "refuse",
|
|
932
|
+
failure: "provider_prompt_mismatch",
|
|
933
|
+
detail: `Guidance was named for campaign ${ref.campaignOfferId}, not the selected ${packet.scope.campaignId}.`,
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
if (chosen.sourceCommand !== null &&
|
|
937
|
+
chosen.sourceCommand.provider !== null &&
|
|
938
|
+
chosen.sourceCommand.provider !== ref.provider) {
|
|
939
|
+
return {
|
|
940
|
+
kind: "refuse",
|
|
941
|
+
failure: "provider_prompt_mismatch",
|
|
942
|
+
detail: `Guidance was named for ${ref.provider} but the command targets ${chosen.sourceCommand.provider}.`,
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
return { kind: "load", provider: ref.provider };
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* A receipt is the ONLY evidence an effect happened. Missing or uncertain
|
|
949
|
+
* evidence is never success, and an uncertain receipt is reconciled by effect
|
|
950
|
+
* identity rather than assumed to be a no-op.
|
|
951
|
+
*/
|
|
952
|
+
function validateReceipt(receipt, request, chosen) {
|
|
953
|
+
if (receipt.outcome === "uncertain") {
|
|
954
|
+
return {
|
|
955
|
+
failure: "delegated_tool_timeout",
|
|
956
|
+
detail: `The ${request.action} authority returned an uncertain effect; reconcile by effect identity before any further mutation.`,
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
if (receipt.outcome === "refused") {
|
|
960
|
+
return {
|
|
961
|
+
failure: "receipt_incomplete",
|
|
962
|
+
detail: `The ${request.action} authority refused: ${receipt.refusalReason ?? "no reason supplied"}.`,
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
if (!receipt.reobserveRequired) {
|
|
966
|
+
return {
|
|
967
|
+
failure: "receipt_incomplete",
|
|
968
|
+
detail: "Every closed outcome obliges a fresh exact sender/date observation.",
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
if (receipt.outcome === "applied") {
|
|
972
|
+
const missing = [];
|
|
973
|
+
if (!isNonEmptyString(receipt.beforeFingerprint))
|
|
974
|
+
missing.push("beforeFingerprint");
|
|
975
|
+
if (!isNonEmptyString(receipt.afterFingerprint))
|
|
976
|
+
missing.push("afterFingerprint");
|
|
977
|
+
if (receipt.changedFields.length === 0)
|
|
978
|
+
missing.push("changedFields");
|
|
979
|
+
if (!isNonEmptyString(receipt.effectKey))
|
|
980
|
+
missing.push("effectKey");
|
|
981
|
+
if (receipt.rollback === null)
|
|
982
|
+
missing.push("rollback");
|
|
983
|
+
if (missing.length > 0) {
|
|
984
|
+
return {
|
|
985
|
+
failure: "receipt_incomplete",
|
|
986
|
+
detail: `An applied receipt is missing: ${missing.join(", ")}.`,
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
if (chosen.action === ACTION.regenerateStaleMessages) {
|
|
991
|
+
const delegation = receipt.preparationDelegation;
|
|
992
|
+
const absent = REFILL_V3_PREPARATION_REUSE_POINTS.filter((point) => delegation === null || delegation[point] === undefined);
|
|
993
|
+
if (absent.length > 0) {
|
|
994
|
+
return {
|
|
995
|
+
failure: "receipt_incomplete",
|
|
996
|
+
detail: `Message preparation must consume every reuse point; missing: ${absent.join(", ")}.`,
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
return null;
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Exported so the falsification path can never be restated: the AI's exhaustion
|
|
1004
|
+
* claim is answered by the SHARED predicate, not by a local truth table.
|
|
1005
|
+
*/
|
|
1006
|
+
export function falsifyRefillV3ExhaustionClaim(evidence) {
|
|
1007
|
+
return REFILL_V3_EXHAUSTION_PREDICATE.evaluate(evidence);
|
|
1008
|
+
}
|
|
1009
|
+
/** The bounded sample cap, re-exported so no consumer computes a cohort. */
|
|
1010
|
+
export const REFILL_V3_CAMPAIGN_WORK_SAMPLE_CAP = REFILL_V3_DIAGNOSTIC_SAMPLE_CAP;
|
|
1011
|
+
/** Guard used by the decoder and by the source dispatcher's boundary pins. */
|
|
1012
|
+
export function isRefillV3CampaignAttentionAction(value) {
|
|
1013
|
+
return (typeof value === "string" &&
|
|
1014
|
+
REFILL_V3_CAMPAIGN_ATTENTION_ACTIONS.includes(value));
|
|
1015
|
+
}
|