@sellable/mcp 0.1.634 → 0.1.635

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.
@@ -480,7 +480,16 @@ function doneReasonFromPlan(plan) {
480
480
  const action = firstAction(plan);
481
481
  return (stringValue(action?.doneReason) ??
482
482
  stringValue(recordValue(plan.packet)?.doneReason) ??
483
- stringValue(plan.doneReason));
483
+ stringValue(plan.doneReason) ??
484
+ (canonicalReportingCoverageComplete(plan) ? "complete" : null));
485
+ }
486
+ function canonicalReportingCoverageComplete(value) {
487
+ const reporting = reportingFrom(value);
488
+ const terminal = recordValue(reporting?.terminal);
489
+ return (reporting?.schemaVersion === "refill_reporting.v2" &&
490
+ terminal?.status === "complete" &&
491
+ terminal.complete === true &&
492
+ terminal.reason === "coverage_complete");
484
493
  }
485
494
  function selectedLaneAuthority(plan, action) {
486
495
  const senderId = actionSenderId(action);
@@ -538,8 +547,7 @@ function isSelectedSenderOwnedPostEngagersLane(plan, action) {
538
547
  const selections = arrayValue(evergreen.senderLaneSelections).filter(isRecord);
539
548
  const selection = selections.find((entry) => stringValue(entry.senderId) === senderId);
540
549
  const selectedLane = recordValue(selection?.selectedLane);
541
- if (!selectedLane ||
542
- stringValue(selectedLane.campaignId) !== campaignId) {
550
+ if (!selectedLane || stringValue(selectedLane.campaignId) !== campaignId) {
543
551
  return false;
544
552
  }
545
553
  const laneKey = stringValue(selectedLane.laneKey);
@@ -1988,6 +1996,13 @@ async function gatePlan(input, deps, ctx) {
1988
1996
  report: plan,
1989
1997
  };
1990
1998
  }
1999
+ // Canonical reporting completion wins over a stale queue head. Check it
2000
+ // before packet coherence so `terminal_with_action_queue` cannot turn a
2001
+ // completed fenced run into a permanent blocker/continuation loop.
2002
+ if (canonicalReportingCoverageComplete(plan)) {
2003
+ ctx.plan = plan;
2004
+ return completeTerminal(input, deps, ctx, "complete", { plan });
2005
+ }
1991
2006
  const coherenceFailure = packetCoherenceFailure(plan);
1992
2007
  if (coherenceFailure) {
1993
2008
  await safeJournalAppend(ctx, deps, deps.journal.renderPlanSection({
@@ -3518,8 +3533,7 @@ async function gateVerify(input, deps, ctx, budgets) {
3518
3533
  // outer workspace coordinator can skip this target and preflight the next
3519
3534
  // ranked campaign behind a new fence. Trying to replan across campaigns
3520
3535
  // inside this fence would correctly fail target_config_mismatch.
3521
- if (exhaustionReason === "sender_owned_post_required" &&
3522
- ctx.targetConfig) {
3536
+ if (exhaustionReason === "sender_owned_post_required" && ctx.targetConfig) {
3523
3537
  ctx.runState = nextRunState;
3524
3538
  return completeTerminal(input, deps, ctx, "lanes_exhausted", {
3525
3539
  laneExhaustion: {
@@ -3758,7 +3772,25 @@ async function finalizeVerifyOrHeartbeat(input, deps, ctx, budgets) {
3758
3772
  if (verified)
3759
3773
  return verified;
3760
3774
  // gateVerify advanced the gate off g3_verify (typically to g1_plan); fall
3761
- // through and heartbeat at the clean, resumable re-entry point.
3775
+ // through and heartbeat at the clean, resumable re-entry point. Before
3776
+ // yielding, perform one read-only completion check: the verified primitive
3777
+ // may have filled the final slots, and emitting an active continuation in
3778
+ // that state was the Nick Bird coverage_complete loop.
3779
+ if (ctx.gate === "g1_plan") {
3780
+ try {
3781
+ const fresh = await deps.readPlan(refillPlanReadInput(input, { ctx, journal: false }));
3782
+ ctx.plan = fresh;
3783
+ if (canonicalReportingCoverageComplete(fresh)) {
3784
+ return completeTerminal(input, deps, ctx, "complete", {
3785
+ plan: fresh,
3786
+ });
3787
+ }
3788
+ }
3789
+ catch {
3790
+ // The ordinary heartbeat remains the safe fallback when the optional
3791
+ // terminal reread is temporarily unavailable.
3792
+ }
3793
+ }
3762
3794
  }
3763
3795
  return heartbeatAndContinue(input, deps, ctx);
3764
3796
  }
@@ -599,8 +599,17 @@ function evergreenExactTargetCandidates(workspacePlan, options) {
599
599
  }
600
600
  async function selectWorkspaceExactTarget(input, workspacePlanOverride, options = {}) {
601
601
  const workspaceInput = workspaceCoordinatorInput(input);
602
- const workspacePlan = workspacePlanOverride ??
602
+ let workspacePlan = workspacePlanOverride ??
603
603
  (await getRefillTargetPlan(targetPlanInputFor(workspaceInput)));
604
+ // A workspace-wide inference read can briefly observe every enrolled sender
605
+ // as disconnected while the authoritative account status is converging. Do
606
+ // one fresh, read-only planner pass before turning that transient snapshot
607
+ // into a terminal blocker. A stable disconnection still blocks on the second
608
+ // receipt and no mutation is attempted.
609
+ if (workspaceConnectivityEmptyState(workspacePlan) &&
610
+ workspacePlanOverride === undefined) {
611
+ workspacePlan = await getRefillTargetPlan(targetPlanInputFor(workspaceInput));
612
+ }
604
613
  if (yoloPlanIsComplete(workspacePlan)) {
605
614
  return { status: "complete", workspacePlan };
606
615
  }
@@ -628,8 +637,7 @@ async function selectWorkspaceExactTarget(input, workspacePlanOverride, options
628
637
  preferredSenderId: options.preferredSenderId,
629
638
  actionTypes: input.actionTypes,
630
639
  }),
631
- ]
632
- .find((candidate) => Boolean(candidate) &&
640
+ ].find((candidate) => Boolean(candidate) &&
633
641
  !excludedTargets.some((excluded) => excluded.campaignId === candidate?.campaignId &&
634
642
  excluded.tableId === candidate?.tableId));
635
643
  if (!selectedTarget) {
@@ -714,7 +722,21 @@ function yoloCoverage(plan) {
714
722
  }
715
723
  function yoloPlanIsComplete(plan) {
716
724
  const coverage = yoloCoverage(plan);
717
- return coverage.status === "complete" || coverage.remainingProjectedGap === 0;
725
+ return (coverage.status === "complete" ||
726
+ (coverage.status !== "blocked" &&
727
+ typeof coverage.grossTarget === "number" &&
728
+ coverage.grossTarget > 0 &&
729
+ coverage.remainingProjectedGap === 0));
730
+ }
731
+ function workspaceConnectivityEmptyState(plan) {
732
+ const target = yoloTarget(plan);
733
+ const emptyState = recordValue(target?.emptyState);
734
+ const blockerCodes = Array.isArray(emptyState?.blockerCodes)
735
+ ? normalizeStrings(emptyState.blockerCodes)
736
+ : [];
737
+ return (stringValue(recordValue(plan)?.status) === "blocked" &&
738
+ emptyState?.code === "no_connected_eligible_senders" &&
739
+ blockerCodes.includes("sender_disconnected"));
718
740
  }
719
741
  function yoloActionIdentity(action) {
720
742
  if (!action)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.634",
3
+ "version": "0.1.635",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code, Codex, and Hermes campaign workflows",
6
6
  "main": "dist/index.js",