@sellable/mcp 0.1.736 → 0.1.738

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.
@@ -166,6 +166,11 @@ declare function dominantSchedulerRefusalReason(receipt: Record<string, unknown>
166
166
  reason: string;
167
167
  count: number;
168
168
  } | null;
169
+ type SchedulerRefusalDisposition = {
170
+ retryable: boolean | null;
171
+ blockerClass: "transient_or_refreshable" | "time_bound_capacity" | "operator_action_required" | "unknown";
172
+ };
173
+ declare function classifySchedulerRefusal(reason: string | null): SchedulerRefusalDisposition;
169
174
  declare function schedulerSweepNoOpBlocker(ctx: LoopContext, reportInput: Record<string, unknown>): Record<string, unknown> | null;
170
175
  export declare function packetCoherenceFailure(plan: Record<string, unknown>): string | null;
171
176
  export declare function terminalProjection(value: {
@@ -184,5 +189,6 @@ export declare function runRefillV2Loop(input: RefillV2LoopInput, deps: RefillV2
184
189
  export declare const refillRunLoopInternals: {
185
190
  schedulerSweepNoOpBlocker: typeof schedulerSweepNoOpBlocker;
186
191
  dominantSchedulerRefusalReason: typeof dominantSchedulerRefusalReason;
192
+ classifySchedulerRefusal: typeof classifySchedulerRefusal;
187
193
  };
188
194
  export {};
@@ -432,6 +432,24 @@ function dominantSchedulerRefusalReason(receipt) {
432
432
  }
433
433
  return null;
434
434
  }
435
+ function classifySchedulerRefusal(reason) {
436
+ if (!reason)
437
+ return { retryable: null, blockerClass: "unknown" };
438
+ // Infrastructure or freshness failures: the underlying facts can be re-read
439
+ // and the same sweep can then succeed without anyone changing configuration.
440
+ if (/(pool|timeout|timed[_ -]?out|connection|transient|unavailable|receipt[_ -]?invalid|stale|refresh)/i.test(reason)) {
441
+ return { retryable: true, blockerClass: "transient_or_refreshable" };
442
+ }
443
+ // Time-bound capacity: nothing is broken, the slot simply is not open yet.
444
+ if (/(window|capacity|daily[_ -]?limit|cooldown|rate[_ -]?limit)/i.test(reason)) {
445
+ return { retryable: true, blockerClass: "time_bound_capacity" };
446
+ }
447
+ // Standing gates: re-running cannot clear these; a human must act.
448
+ if (/(billing|credit[_ -]?threshold|disconnected|sender[_ -]?mismatch|not[_ -]?scheduler[_ -]?eligible|suspended|unauthor)/i.test(reason)) {
449
+ return { retryable: false, blockerClass: "operator_action_required" };
450
+ }
451
+ return { retryable: null, blockerClass: "unknown" };
452
+ }
435
453
  // fix(112al): a sweep whose own receipt proves it placed zero while ready
436
454
  // inventory existed. Gated on the executed action actually being a scheduler
437
455
  // sweep and on a FRESH terminal receipt (ran / window_closed_noop), so an
@@ -461,8 +479,9 @@ function schedulerSweepNoOpBlocker(ctx, reportInput) {
461
479
  dominantReason: dominant.reason,
462
480
  dominantReasonSource: dominant.source,
463
481
  dominantReasonCount: dominant.count,
482
+ ...classifySchedulerRefusal(dominant.reason),
464
483
  }
465
- : {}),
484
+ : classifySchedulerRefusal(null)),
466
485
  ...(stringValue(receipt?.nextAction)
467
486
  ? { recommendedAction: stringValue(receipt?.nextAction) }
468
487
  : {}),
@@ -4806,4 +4825,5 @@ export async function runRefillV2Loop(input, deps) {
4806
4825
  export const refillRunLoopInternals = {
4807
4826
  schedulerSweepNoOpBlocker,
4808
4827
  dominantSchedulerRefusalReason,
4828
+ classifySchedulerRefusal,
4809
4829
  };
@@ -22,7 +22,7 @@ export const refreshSenderEngagementToolDefinitions = [
22
22
  },
23
23
  campaignId: {
24
24
  type: "string",
25
- description: "Optional exact sender-owned Post Engagers campaign id. Enables the same safe refresh path for a campaign-derived waterfall without requiring managed evergreen configuration.",
25
+ description: "Optional exact sender-owned Post Engagers campaign id. Omit it and the command discovers the sender's Post Engagers campaign itself, so managed evergreen configuration is not required; pass it only to pin one exact campaign. When the sender owns several lanes, the response reports the rest under discovery.additionalCandidates — refresh each by repeating the call with that campaignId.",
26
26
  },
27
27
  tableId: {
28
28
  type: "string",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.736",
3
+ "version": "0.1.738",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code, Codex, and Hermes campaign workflows",
6
6
  "main": "dist/index.js",
@@ -33,7 +33,9 @@ You are a pipeline supply agent. People who engage with a sender's LinkedIn post
33
33
  <inputs>
34
34
  The invoking prompt names the senders ("refresh sender engagement for csreyes92 and thomas"). Resolve each via `list_senders` (match name/handle/LinkedIn URL). With no names given, inspect the active workspace and refresh every connected sender that has an active/paused sender-owned Post Engagers campaign backed by Signal Discovery.
35
35
 
36
- Optional: target sender names/ids, `tableId` when the user wants to force a specific campaign table, maximum posts per sender (default 5, hard cap 5 unless the user explicitly asks for more), and maximum engager pages per tracked post.
36
+ Optional: target sender names/ids, `campaignId` when the user wants to force a specific Post Engagers campaign, `tableId` when the user wants to force a specific campaign table, maximum posts per sender (default 5, hard cap 5 unless the user explicitly asks for more), and maximum engager pages per tracked post.
37
+
38
+ `campaignId` is optional. When it is omitted the command discovers the sender's Post Engagers campaign itself — it searches the workspace for campaigns the sender solely owns that are backed by a post-engager provider and named as a Post Engagers lane. A workspace can hold one such lane per sender, so discovery is always scoped to the sender you are refreshing; another sender's lane and shared multi-sender lanes are never selected. Do not pass a `campaignId` you guessed.
37
39
  </inputs>
38
40
 
39
41
  <entrypoint>
@@ -58,9 +60,12 @@ For each target sender/campaign:
58
60
  - Resolve each target sender with `list_senders`/`get_sender`. Use the sender id returned by the product, not a guessed handle.
59
61
  2. **Use the typed product command first**:
60
62
  - Call `refresh_sender_engagement` in `mode:"dry_run"` for each sender.
61
- - Always pass `workspaceId`, `senderId`, and any user-specified `tableId`, `maxPosts`, or `maxEngagerPages`.
63
+ - Always pass `workspaceId`, `senderId`, and any user-specified `campaignId`, `tableId`, `maxPosts`, or `maxEngagerPages`. Omitting `campaignId` is correct and normal — the command resolves the campaign itself.
64
+ - Read `discovery` on the response to see how the campaign was resolved (`managed_slot`, `explicit_campaign_id`, or `auto_discovered`) and which one was selected.
65
+ - If `discovery.additionalCandidates` is non-empty, that sender owns more than one Post Engagers lane. Refresh each one by repeating the dry-run/apply cycle with that candidate's `campaignId`. Every lane gets its own dry run and its own `dryRunFingerprint`; never reuse a fingerprint across campaigns.
62
66
  - Treat the dry-run response as the campaign/source boundary authority. It should identify the sender-owned Post Engagers slot, campaign, workflow table, source provider `campaign-tracked-post`, source table type `tracked_post_engager_source_list`, expected tracked posts, expected engager refresh/import work, and a `dryRunFingerprint`.
63
67
  - If the typed command reports no eligible sender-owned Post Engagers campaign, no tracked posts, no recent posts, or a workspace/sender mismatch, report that exact blocker and stop for that sender. Do not silently create campaigns or fall back to shared lanes.
68
+ - `sender_owned_post_engagers_slot_not_found` now means discovery genuinely found no eligible lane, not that a campaign id was missing. Report `discovery.campaignsInspected` alongside it, and describe it as missing campaign setup — never as "the sender has no posts", since no LinkedIn fetch happens before this point.
64
69
  3. **Gate writes with dry-run proof**:
65
70
  - Never call `refresh_sender_engagement` in `mode:"apply"` before a successful dry run from this same run.
66
71
  - For manual runs, show the target workspace, sender, campaign/table, source lead list, expected tracked posts, expected row/import impact, and `dryRunFingerprint`, then wait for explicit user approval before apply.
@@ -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
+ }