@sellable/mcp 0.1.570 → 0.1.572

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.
@@ -1,5 +1,9 @@
1
+ import { createHash } from "crypto";
1
2
  import { getApi } from "../api.js";
2
3
  import { normalizeExplicitWorkspaceId, workspaceRequestOptions, } from "./workspace-context.js";
4
+ export const REFILL_SCHEDULER_EXPECTED_TARGET_LIMIT = 25;
5
+ export const REFILL_SCHEDULER_EXPECTED_TARGET_SENDER_LIMIT = 100;
6
+ export const REFILL_SCHEDULER_MAX_PLACEMENTS = 150;
3
7
  async function postSchedulerRun(body, workspaceId) {
4
8
  const api = getApi();
5
9
  const requestOptions = workspaceRequestOptions(workspaceId);
@@ -29,10 +33,89 @@ function normalizeOptionalScopeValue(value, field) {
29
33
  }
30
34
  return value.trim();
31
35
  }
36
+ function canonicalizeExpectedTargets(value) {
37
+ if (!Array.isArray(value) || value.length === 0) {
38
+ throw new Error("expectedTargets must be a non-empty array.");
39
+ }
40
+ if (value.length > REFILL_SCHEDULER_EXPECTED_TARGET_LIMIT) {
41
+ throw new Error(`expected_target_overflow: expectedTargets exceeds ${REFILL_SCHEDULER_EXPECTED_TARGET_LIMIT}.`);
42
+ }
43
+ const supportedActions = new Set([
44
+ "send_invite",
45
+ "send_inmail_open",
46
+ "send_inmail_closed",
47
+ ]);
48
+ const canonical = value.map((rawTarget, index) => {
49
+ if (!rawTarget ||
50
+ typeof rawTarget !== "object" ||
51
+ Array.isArray(rawTarget)) {
52
+ throw new Error(`expectedTargets[${index}] must be an object.`);
53
+ }
54
+ const target = rawTarget;
55
+ const allowedKeys = new Set([
56
+ "campaignId",
57
+ "tableId",
58
+ "refillLaneKey",
59
+ "branchActionTypes",
60
+ "senderIds",
61
+ ]);
62
+ if (Object.keys(target).some((key) => !allowedKeys.has(key))) {
63
+ throw new Error(`expectedTargets[${index}] contains unsupported fields.`);
64
+ }
65
+ const campaignId = normalizeOptionalScopeValue(target.campaignId, `expectedTargets[${index}].campaignId`);
66
+ const tableId = normalizeOptionalScopeValue(target.tableId, `expectedTargets[${index}].tableId`);
67
+ const refillLaneKey = normalizeOptionalScopeValue(target.refillLaneKey, `expectedTargets[${index}].refillLaneKey`);
68
+ if (!campaignId ||
69
+ !tableId ||
70
+ !refillLaneKey ||
71
+ !Array.isArray(target.branchActionTypes) ||
72
+ target.branchActionTypes.length === 0 ||
73
+ !Array.isArray(target.senderIds) ||
74
+ target.senderIds.length === 0) {
75
+ throw new Error(`expectedTargets[${index}] is incomplete.`);
76
+ }
77
+ if (target.senderIds.length > REFILL_SCHEDULER_EXPECTED_TARGET_SENDER_LIMIT) {
78
+ throw new Error(`expected_target_overflow: expectedTargets[${index}].senderIds exceeds ${REFILL_SCHEDULER_EXPECTED_TARGET_SENDER_LIMIT}.`);
79
+ }
80
+ const branchActionTypes = target.branchActionTypes.map((actionType) => {
81
+ if (typeof actionType !== "string" || !supportedActions.has(actionType)) {
82
+ throw new Error(`expectedTargets[${index}] contains an unsupported branch action.`);
83
+ }
84
+ return actionType;
85
+ });
86
+ const senderIds = target.senderIds.map((senderId) => {
87
+ const normalized = normalizeOptionalScopeValue(senderId, `expectedTargets[${index}].senderIds`);
88
+ if (!normalized) {
89
+ throw new Error(`expectedTargets[${index}].senderIds must be non-empty.`);
90
+ }
91
+ return normalized;
92
+ });
93
+ if (new Set(branchActionTypes).size !== branchActionTypes.length ||
94
+ new Set(senderIds).size !== senderIds.length) {
95
+ throw new Error(`expectedTargets[${index}] branchActionTypes and senderIds must be unique.`);
96
+ }
97
+ return {
98
+ campaignId,
99
+ tableId,
100
+ refillLaneKey,
101
+ branchActionTypes: branchActionTypes.sort(),
102
+ senderIds: senderIds.sort(),
103
+ };
104
+ });
105
+ canonical.sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
106
+ const targetKeys = canonical.map((target) => JSON.stringify(target));
107
+ if (new Set(targetKeys).size !== targetKeys.length) {
108
+ throw new Error("expectedTargets contains duplicate canonical targets.");
109
+ }
110
+ return canonical;
111
+ }
112
+ function computeExpectedTargetsHash(targets) {
113
+ return createHash("sha256").update(JSON.stringify(targets)).digest("hex");
114
+ }
32
115
  export const schedulerRunToolDefinitions = [
33
116
  {
34
117
  name: "run_scheduler_sweep",
35
- description: 'Trigger the product scheduler placement sweep for one explicit workspace now, or read the last on-demand scheduler run status with action "status". A run is workspace-wide, not a scoped run for one campaign/table, and returns a synchronous envelope with status ran, attached, backoff, window_closed_noop, or failed; retryAfterMs for backoff replays; and a backward-compatible receipt. In v2 receipts, readyCellsFound is total scheduler-ready supply found before filters, cellsConsidered is the allocation-attempt count that survived prefilters, prefiltered means ready cells removed before allocation such as no capacity, stale paid InMail credit facts, or sender mismatch, skipped means considered cells rejected by hard scheduler gates, and deferred means considered cells waiting on windows/capacity/cooldown. campaignScopeSummary lists bounded tables/campaigns the workspace-wide run inspected; absence there is not a scoped-run guarantee that the target was ready. The receipt can include readyCellsByType, consideredCellsByType, prefilterReasons, skippedReasons, deferredReasons, summary, and nextAction. It places cells within existing scheduler gates only: it can move placement earlier, but it cannot bypass sending windows, daily limits, cooldowns, sender gates, billing, or credit thresholds, and it never sends messages directly. Repeated calls inside the backoff window replay the last receipt verbatim. Status is read-only and scoped to the last on-demand run only; cron sweeps are not recorded here.',
118
+ description: 'Trigger the product scheduler placement sweep for one explicit workspace now, or read the last on-demand scheduler run status with action "status". A run is workspace-wide, not a scoped run for one campaign/table, and returns a synchronous envelope with status ran, attached, backoff, window_closed_noop, or failed; retryAfterMs for backoff replays; and a backward-compatible receipt. In v3 receipts, readyCellsFound is total scheduler-ready supply found before filters, cellsConsidered is the allocation-attempt count that survived prefilters, prefiltered means ready cells removed before allocation such as no capacity, stale paid InMail credit facts, or sender mismatch, skipped means considered cells rejected by hard scheduler gates, and deferred means considered cells waiting on windows/capacity/cooldown. campaignScopeSummary remains bounded display evidence, while expectedTargetSummary explains every hash-bound planner target exactly once without filtering placement. Expected targets require a frozen numeric total placement cap; unrelated eligible campaigns may consume that cap through normal workspace-wide gates, and changedCounts audits every actual placement by campaign, table, action, and sender. The receipt can include readyCellsByType, consideredCellsByType, prefilterReasons, skippedReasons, deferredReasons, summary, and nextAction. It places cells within existing scheduler gates only: it can move placement earlier, but it cannot bypass sending windows, daily limits, cooldowns, sender gates, billing, or credit thresholds, and it never sends messages directly. Repeated calls inside the backoff window replay the last receipt verbatim. Status is read-only and scoped to the last on-demand run only; cron sweeps are not recorded here.',
36
119
  inputSchema: {
37
120
  type: "object",
38
121
  properties: {
@@ -57,6 +140,56 @@ export const schedulerRunToolDefinitions = [
57
140
  type: "string",
58
141
  description: "Optional stable approved refill target-shape revision associated with requestKey.",
59
142
  },
143
+ expectedTargets: {
144
+ type: "array",
145
+ maxItems: REFILL_SCHEDULER_EXPECTED_TARGET_LIMIT,
146
+ items: {
147
+ type: "object",
148
+ properties: {
149
+ campaignId: { type: "string" },
150
+ tableId: { type: "string" },
151
+ refillLaneKey: { type: "string" },
152
+ branchActionTypes: {
153
+ type: "array",
154
+ minItems: 1,
155
+ uniqueItems: true,
156
+ items: {
157
+ type: "string",
158
+ enum: [
159
+ "send_invite",
160
+ "send_inmail_open",
161
+ "send_inmail_closed",
162
+ ],
163
+ },
164
+ },
165
+ senderIds: {
166
+ type: "array",
167
+ minItems: 1,
168
+ maxItems: REFILL_SCHEDULER_EXPECTED_TARGET_SENDER_LIMIT,
169
+ uniqueItems: true,
170
+ items: { type: "string" },
171
+ },
172
+ },
173
+ required: [
174
+ "campaignId",
175
+ "tableId",
176
+ "refillLaneKey",
177
+ "branchActionTypes",
178
+ "senderIds",
179
+ ],
180
+ additionalProperties: false,
181
+ },
182
+ },
183
+ expectedTargetsHash: {
184
+ type: "string",
185
+ description: "Required SHA-256 of canonical expectedTargets for a target-aware run.",
186
+ },
187
+ maxPlacements: {
188
+ type: "integer",
189
+ minimum: 1,
190
+ maximum: REFILL_SCHEDULER_MAX_PLACEMENTS,
191
+ description: "Required frozen numeric total placement cap across the workspace-wide sweep when expectedTargets are present.",
192
+ },
60
193
  },
61
194
  required: ["workspaceId"],
62
195
  additionalProperties: false,
@@ -75,11 +208,60 @@ export async function runSchedulerSweep(input) {
75
208
  if (action !== "run" && action !== "status") {
76
209
  throw new Error('action must be "run" or "status" for run_scheduler_sweep.');
77
210
  }
211
+ const hasExpectedTargets = input.expectedTargets !== undefined;
212
+ const hasExpectedTargetScope = input.expectedTargetsHash !== undefined ||
213
+ input.maxPlacements !== undefined;
214
+ if (action === "status" && hasExpectedTargets) {
215
+ throw new Error("expectedTargets are only valid for action run.");
216
+ }
217
+ let expectedTargetRequest;
218
+ if (action === "run" && (hasExpectedTargets || hasExpectedTargetScope)) {
219
+ const expectedTargets = canonicalizeExpectedTargets(input.expectedTargets);
220
+ const expectedTargetsHash = normalizeOptionalScopeValue(input.expectedTargetsHash, "expectedTargetsHash");
221
+ if (!expectedTargetsHash ||
222
+ expectedTargetsHash !== computeExpectedTargetsHash(expectedTargets)) {
223
+ throw new Error("expectedTargetsHash must match the canonical expectedTargets payload.");
224
+ }
225
+ if (typeof input.maxPlacements !== "number" ||
226
+ !Number.isInteger(input.maxPlacements) ||
227
+ input.maxPlacements <= 0 ||
228
+ input.maxPlacements > REFILL_SCHEDULER_MAX_PLACEMENTS) {
229
+ throw new Error(`maxPlacements must be an integer from 1 to ${REFILL_SCHEDULER_MAX_PLACEMENTS}.`);
230
+ }
231
+ if (!targetDate || !requestKey || !targetShapeRevision) {
232
+ throw new Error("expectedTargets require targetDate, requestKey, and targetShapeRevision.");
233
+ }
234
+ expectedTargetRequest = {
235
+ expectedTargets,
236
+ expectedTargetsHash,
237
+ maxPlacements: input.maxPlacements,
238
+ };
239
+ }
240
+ let expectedStatusScope;
241
+ if (action === "status" && hasExpectedTargetScope) {
242
+ const expectedTargetsHash = normalizeOptionalScopeValue(input.expectedTargetsHash, "expectedTargetsHash");
243
+ if (!expectedTargetsHash ||
244
+ typeof input.maxPlacements !== "number" ||
245
+ !Number.isInteger(input.maxPlacements) ||
246
+ input.maxPlacements <= 0 ||
247
+ input.maxPlacements > REFILL_SCHEDULER_MAX_PLACEMENTS ||
248
+ !targetDate ||
249
+ !requestKey ||
250
+ !targetShapeRevision) {
251
+ throw new Error("hash-bound status requires expectedTargetsHash, maxPlacements, targetDate, requestKey, and targetShapeRevision.");
252
+ }
253
+ expectedStatusScope = {
254
+ expectedTargetsHash,
255
+ maxPlacements: input.maxPlacements,
256
+ };
257
+ }
78
258
  return postSchedulerRun({
79
259
  workspaceId,
80
260
  action,
81
261
  ...(targetDate ? { targetDate } : {}),
82
262
  ...(requestKey ? { requestKey } : {}),
83
263
  ...(targetShapeRevision ? { targetShapeRevision } : {}),
264
+ ...(expectedTargetRequest ?? {}),
265
+ ...(expectedStatusScope ?? {}),
84
266
  }, workspaceId);
85
267
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.570",
3
+ "version": "0.1.572",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code, Codex, and Hermes campaign workflows",
6
6
  "main": "dist/index.js",
@@ -223,13 +223,17 @@ Structured planner packet:
223
223
  campaign/sender/lane summary, skipped rungs, existing-row frontier proof, and
224
224
  any absolute `wait.deadlineAt`.
225
225
  - Preserve these coverage labels exactly: `Need to prepare`, `Goal`,
226
- `Already sent`, `Scheduled`, `Ready and waiting to be scheduled`, and
227
- `Still need`.
226
+ `Already sent`, `Scheduled`, `Ready and waiting to be scheduled`,
227
+ `Still need to schedule`, and `Still need to prepare`. Never render an
228
+ unlabeled `Still need` value.
228
229
  - `target.globalActionQueue` is the only cross-sender yolo execution queue.
229
230
  Execute exactly one globally ranked primitive from
230
231
  `target.globalActionQueue[0]`, then rerun `get_refill_target_plan` with the
231
232
  same `workspaceId` before
232
233
  choosing another action.
234
+ - When that primitive is `run_scheduler_sweep`, show `workspaceWide:true` and
235
+ its reason in the approval/yolo packet so the operator sees that the sweep
236
+ may schedule other eligible workspace cells on the approved date.
233
237
  - `manualAlternates` are not yolo actions. Threshold lowering and campaign
234
238
  creation are manual continuations only.
235
239
 
@@ -324,12 +328,16 @@ If the target plan is complete by projected coverage, report that the selected
324
328
  target is already filled and no-op without asking for approval. If the ready
325
329
  buffer covers the projected gap, paid InMail credit facts are fresh for every
326
330
  selected paid-InMail lane, but scheduled coverage is still short, keep the run
327
- open in a persistent read-only scheduler wait loop. Poll
328
- `get_refill_target_plan` every 60-120 seconds, or on the host's next continuation
329
- interval, until projected coverage fills the target, a concrete non-scheduler
330
- blocker appears, or Christian explicitly asks to stop or only receive a status
331
- report. Treat `awaiting_scheduler_after_ready_buffer` as an in-progress wait
332
- state, not a close-out condition.
331
+ open in a persistent read-only scheduler wait loop. Honor the emitted wait
332
+ metadata: reread `get_refill_target_plan` after about 30 seconds by default (or
333
+ the action's explicit `rereadAfterMs`), never use a blind hard-coded sleep, and
334
+ stop at the named bound using `receipt_deadline > unchanged_poll_bound >
335
+ run_budget` precedence. A matching active/uncertain request key is never
336
+ redispatched. Continue until projected coverage fills the target, a concrete
337
+ non-scheduler blocker appears, a named bound stops the run, or Christian
338
+ explicitly asks to stop or only receive a status report. Treat
339
+ `awaiting_scheduler_after_ready_buffer` as an in-progress wait state, not a
340
+ close-out condition.
333
341
  Wait actions are gates, not competing goals. When `wait_for_active_work` or
334
342
  `wait_for_scheduler` includes receipt `wait.deadlineAt`, honor that absolute
335
343
  deadline; if it is expired on this call, escalate to diagnostics with the
@@ -140,13 +140,17 @@ files or memory.
140
140
  campaign/sender/lane summary, skipped rungs, existing-row frontier proof,
141
141
  and any absolute `wait.deadlineAt`.
142
142
  - Preserve these coverage labels exactly: `Need to prepare`, `Goal`,
143
- `Already sent`, `Scheduled`, `Ready and waiting to be scheduled`, and
144
- `Still need`.
143
+ `Already sent`, `Scheduled`, `Ready and waiting to be scheduled`,
144
+ `Still need to schedule`, and `Still need to prepare`. Never render an
145
+ unlabeled `Still need` value.
145
146
  - `target.globalActionQueue` is the only cross-sender yolo execution queue.
146
147
  execute exactly one globally ranked primitive from
147
148
  `target.globalActionQueue[0]`, then rerun `get_refill_target_plan` with
148
149
  the same `workspaceId` before
149
150
  choosing another action.
151
+ - When that primitive is `run_scheduler_sweep`, show `workspaceWide:true`
152
+ and its reason in the approval/yolo packet so the operator sees that the
153
+ sweep may schedule other eligible workspace cells on the approved date.
150
154
  - `nextActions[0]` is the current sender's smallest safe primitive.
151
155
  - `manualAlternates` are not yolo actions. Threshold lowering and campaign
152
156
  creation are manual continuations only.
@@ -0,0 +1,9 @@
1
+ {
2
+ "parallelMode": "wide",
3
+ "agentCount": 6,
4
+ "maxToolCallsPerAgent": 2,
5
+ "senderMaxAgents": 2,
6
+ "senderMaxToolCallsPerAgent": 3,
7
+ "progressMode": true,
8
+ "debugMode": true
9
+ }