@sellable/mcp 0.1.738 → 0.1.740

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.
@@ -172,6 +172,21 @@ type SchedulerRefusalDisposition = {
172
172
  };
173
173
  declare function classifySchedulerRefusal(reason: string | null): SchedulerRefusalDisposition;
174
174
  declare function schedulerSweepNoOpBlocker(ctx: LoopContext, reportInput: Record<string, unknown>): Record<string, unknown> | null;
175
+ declare function coverageOnlySignature(plan: Record<string, unknown>): {
176
+ sent: number;
177
+ scheduled: number;
178
+ ready: number;
179
+ goal: number;
180
+ };
181
+ declare function nextCoverageStallState(ctx: LoopContext, plan: Record<string, unknown>): {
182
+ signature: {
183
+ sent: number;
184
+ scheduled: number;
185
+ ready: number;
186
+ goal: number;
187
+ };
188
+ repeatCount: number;
189
+ };
175
190
  export declare function packetCoherenceFailure(plan: Record<string, unknown>): string | null;
176
191
  export declare function terminalProjection(value: {
177
192
  targetConfig?: unknown;
@@ -190,5 +205,8 @@ export declare const refillRunLoopInternals: {
190
205
  schedulerSweepNoOpBlocker: typeof schedulerSweepNoOpBlocker;
191
206
  dominantSchedulerRefusalReason: typeof dominantSchedulerRefusalReason;
192
207
  classifySchedulerRefusal: typeof classifySchedulerRefusal;
208
+ coverageOnlySignature: typeof coverageOnlySignature;
209
+ nextCoverageStallState: typeof nextCoverageStallState;
210
+ COVERAGE_STALL_LIMIT: number;
193
211
  };
194
212
  export {};
@@ -1864,6 +1864,46 @@ function nextNoProgressState(ctx, plan, action) {
1864
1864
  repeatCount,
1865
1865
  };
1866
1866
  }
1867
+ // fix(112an, UNIVERSAL progress-or-settle): 112aj's bound keys on
1868
+ // noProgressSignature, which includes actionFingerprint (type:campaign:table:
1869
+ // sender:actionKey:effectId). A loop that re-plans a DIFFERENT action each
1870
+ // iteration therefore resets the counter and never trips it, no matter how many
1871
+ // iterations produce zero coverage movement. Both expensive livelocks we hit
1872
+ // had exactly that shape:
1873
+ // - Dotwork s3: 136 refill_sends calls, 21 exact_action_complete, coverage
1874
+ // frozen at {sent:0, scheduled:0, ready:20, goal:50} throughout.
1875
+ // - sellable FINAL: 6 of 7 exact runs returned byte-identical coverage
1876
+ // {goal:180, ready:5, scheduled:9, sent:0}, each under a distinct
1877
+ // next_exact_target, so 112aj saw six "different" actions.
1878
+ // This is the coverage-only companion: it ignores WHICH action ran and asks
1879
+ // only whether the world moved. A command that cannot move sent/scheduled/ready
1880
+ // across COVERAGE_STALL_LIMIT consecutive iterations is burning wall-clock and
1881
+ // credits for nothing and must settle with an honest terminal naming the frozen
1882
+ // counters and the last action it attempted.
1883
+ //
1884
+ // Threshold is deliberately looser than 112aj's 3: distinct actions legitimately
1885
+ // run in sequence before coverage moves (prepare -> approve -> sweep), so a tight
1886
+ // cap would cut off real ladders. Six is above the longest legitimate
1887
+ // zero-movement prefix observed across the 8-workspace matrix (the healthy rows
1888
+ // settle in 0-20 calls; only true stalls exceed it).
1889
+ const COVERAGE_STALL_LIMIT = 6;
1890
+ function coverageOnlySignature(plan) {
1891
+ const coverage = coverageSnapshot(plan);
1892
+ return {
1893
+ sent: coverage.sent,
1894
+ scheduled: coverage.scheduled,
1895
+ ready: coverage.ready,
1896
+ goal: coverage.goal,
1897
+ };
1898
+ }
1899
+ function nextCoverageStallState(ctx, plan) {
1900
+ const next = coverageOnlySignature(plan);
1901
+ const prior = recordValue(runStateProgress(ctx).coverageStall) ?? {};
1902
+ const priorSignature = recordValue(prior.signature);
1903
+ const same = priorSignature !== undefined && safeJson(priorSignature) === safeJson(next);
1904
+ const repeatCount = same ? (numberValue(prior.repeatCount) ?? 1) + 1 : 1;
1905
+ return { signature: next, repeatCount };
1906
+ }
1867
1907
  function sourceHandoffText(action) {
1868
1908
  const campaignId = actionCampaignId(action) ?? "campaign";
1869
1909
  const toolInput = actionToolInput(action);
@@ -2601,6 +2641,11 @@ async function gatePlan(input, deps, ctx) {
2601
2641
  fallbackSummary: line.fallbackSummary,
2602
2642
  }));
2603
2643
  const fingerprint = fingerprintFromPlan(plan);
2644
+ // fix(112an): universal coverage-only bound. Evaluated BEFORE 112aj's
2645
+ // action-scoped bound only for its counter update, but enforced AFTER it so
2646
+ // the more specific typed terminals (broaden keyword handoff, etc.) keep
2647
+ // precedence when they apply. See COVERAGE_STALL_LIMIT.
2648
+ const coverageStall = nextCoverageStallState(ctx, plan);
2604
2649
  const noProgress = nextNoProgressState(ctx, plan, action);
2605
2650
  if (noProgress.repeatCount >= 3) {
2606
2651
  // fix(111y, CloverL): when the stuck rung is a broaden whose derived tier
@@ -2665,14 +2710,33 @@ async function gatePlan(input, deps, ctx) {
2665
2710
  blocker: "no_forward_progress",
2666
2711
  stuckRung: stringValue(action.evergreenRung) ?? stringValue(action.type),
2667
2712
  repeatedHeadAction: action,
2668
- progress: { noProgress },
2713
+ progress: { noProgress, coverageStall },
2669
2714
  guidance: "The same refill rung repeated three times with no coverage, frontier, or lane movement.",
2670
2715
  });
2671
2716
  }
2717
+ // fix(112an): universal bound — the world did not move for
2718
+ // COVERAGE_STALL_LIMIT consecutive iterations, regardless of which action ran
2719
+ // each time. Settle with an honest terminal naming the frozen counters rather
2720
+ // than burning further wall-clock and credits on a command that cannot advance.
2721
+ if (coverageStall.repeatCount >= COVERAGE_STALL_LIMIT) {
2722
+ const frozen = coverageStall.signature;
2723
+ return completeTerminal(input, deps, ctx, "blocked", {
2724
+ blocker: "no_coverage_movement",
2725
+ stuckRung: stringValue(action.evergreenRung) ?? stringValue(action.type),
2726
+ repeatedHeadAction: action,
2727
+ progress: { coverageStall },
2728
+ coverage: frozen,
2729
+ guidance: `Coverage did not move across ${COVERAGE_STALL_LIMIT} consecutive refill iterations ` +
2730
+ `(sent ${frozen.sent}, scheduled ${frozen.scheduled}, ready ${frozen.ready}, ` +
2731
+ `goal ${frozen.goal}); the actions differed each time, so this is a workspace-level ` +
2732
+ "stall rather than one repeated rung. Settling instead of continuing to spend. " +
2733
+ "Re-run after the underlying blocker changes; the last attempted action is reported above.",
2734
+ });
2735
+ }
2672
2736
  const nextRunState = mergeRunState(ctx.runState, {
2673
2737
  lastPlan: fingerprint,
2674
2738
  headAction: action,
2675
- progress: mergeProgress(ctx, { noProgress }),
2739
+ progress: mergeProgress(ctx, { noProgress, coverageStall }),
2676
2740
  });
2677
2741
  const advanced = await deps.runClient.advanceRefillRunGateRemote({
2678
2742
  workspaceId: input.workspaceId,
@@ -4826,4 +4890,7 @@ export const refillRunLoopInternals = {
4826
4890
  schedulerSweepNoOpBlocker,
4827
4891
  dominantSchedulerRefusalReason,
4828
4892
  classifySchedulerRefusal,
4893
+ coverageOnlySignature,
4894
+ nextCoverageStallState,
4895
+ COVERAGE_STALL_LIMIT,
4829
4896
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.738",
3
+ "version": "0.1.740",
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, `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.
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, `maxPosts` to refresh a specific number of posts, and maximum engager pages per tracked post.
37
+
38
+ Leave `maxPosts` unset unless the user asked for a specific number. By default the command refreshes every tracked post published inside the 30-day lookback window, which is what keeps recent posts checked often enough to catch new engagers. A post whose engagement counts have not moved costs a single cheap count probe and is skipped before any engager fetch, so a wider sweep is not a proportionally more expensive one. Passing `maxPosts` narrows that sweep.
37
39
 
38
40
  `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.
39
41
  </inputs>
@@ -84,7 +86,7 @@ For each target sender/campaign:
84
86
  - LinkedIn operations here are read-only fetches plus a typed product import into a campaign table. **No messages are generated, approved, scheduled, or sent by this skill.**
85
87
  - Respect workspace boundaries: only add leads to campaigns in the active workspace, and only for senders that belong to it.
86
88
  - Respect campaign boundaries: only add engagers to the matched sender-owned Post Engagers campaign/table. Do not mix shared-lane engagers into sender-owned campaigns or sender-owned engagers into shared lanes.
87
- - Cap provider usage per run: at most 5 posts × `fetch_post_engagers` per sender. If the invoking automation wants more, it must say so explicitly.
89
+ - Provider usage per run is bounded by the 30-day lookback window plus the count-probe short-circuit: unchanged posts cost one probe and fetch no engagers. Do not widen `maxEngagerPages` beyond what the user asked for.
88
90
  - Never call `start_campaign`, `attach_sequence`, `queue_campaign_cells`, `start_campaign_message_preparation`, approval tools, send/schedule tools, or shared-lane mutation tools.
89
91
  - Never create campaigns, change campaign status, change sender ownership, approve campaign cells, or start message prep as part of this refresh.
90
92
  </safety>