@sellable/mcp 0.1.737 → 0.1.739

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
  };
@@ -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.737",
3
+ "version": "0.1.739",
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.