@sellable/mcp 0.1.558 → 0.1.560

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.
@@ -361,11 +361,17 @@ function actionSenderId(action) {
361
361
  }
362
362
  function fingerprintFromPlan(plan) {
363
363
  const packet = recordValue(plan.packet) ?? {};
364
+ const targetShapeRevision = stringValue(packet.targetShapeRevision) ??
365
+ stringValue(packet.targetRevision) ??
366
+ stringValue(plan.targetShapeRevision) ??
367
+ stringValue(plan.targetRevision);
364
368
  return {
365
- planRevision: stringValue(plan.planRevision),
369
+ // Evergreen packets carry their own semantic plan revision. Regular
370
+ // active-campaign packets intentionally do not, so their immutable target
371
+ // shape is the durable planning revision for fencing and recovery.
372
+ planRevision: stringValue(plan.planRevision) ?? targetShapeRevision,
366
373
  stateRevision: stringValue(plan.stateRevision) ?? stringValue(packet.stateRevision),
367
- targetShapeRevision: stringValue(packet.targetShapeRevision) ??
368
- stringValue(packet.targetRevision),
374
+ targetShapeRevision,
369
375
  };
370
376
  }
371
377
  function fingerprintChanged(expected, actual) {
@@ -1021,6 +1027,23 @@ async function gatePlan(input, deps, ctx) {
1021
1027
  fallbackSummary: line.fallbackSummary,
1022
1028
  }));
1023
1029
  const fingerprint = fingerprintFromPlan(plan);
1030
+ const approvedTargetShapeRevision = stringValue(input.approval?.targetShapeRevision);
1031
+ if (approvedTargetShapeRevision &&
1032
+ approvedTargetShapeRevision !== fingerprint.targetShapeRevision) {
1033
+ return {
1034
+ status: "blocked",
1035
+ blocker: "scope_drift",
1036
+ runId: ctx.runId,
1037
+ fence: ctx.fence,
1038
+ gate: ctx.gate,
1039
+ guidance: "The canonical refill target shape changed after approval; render a new packet before another primitive.",
1040
+ report: {
1041
+ approvedTargetShapeRevision,
1042
+ freshTargetShapeRevision: fingerprint.targetShapeRevision,
1043
+ },
1044
+ journalPath: ctx.journalPath,
1045
+ };
1046
+ }
1024
1047
  const persistedCircuit = recordValue(runStateProgress(ctx).prepCircuit);
1025
1048
  if (stringValue(action.type) === "prepare_messages" &&
1026
1049
  openCircuitWaits(persistedCircuit, deps.now?.() ?? new Date())) {
@@ -167,6 +167,17 @@ export declare function prepareRefillSendsV2Approval(input: RefillV2ApprovalPrep
167
167
  response?: undefined;
168
168
  }>;
169
169
  export declare function refillSendsV2Command(input: RefillSendsV2Input): Promise<{
170
+ status: "blocked";
171
+ blocker: "approval_expired" | "approval_fingerprint_mismatch";
172
+ guidance: string;
173
+ report: {
174
+ fresh?: {} | null | undefined;
175
+ changed: string[];
176
+ };
177
+ readOnly: boolean;
178
+ forbiddenActions: string[];
179
+ boundedAuthority: string;
180
+ } | {
170
181
  readOnly: boolean;
171
182
  mode: string;
172
183
  forbiddenActions: string[];
@@ -325,7 +325,11 @@ function validateApprovalEcho(envelope, echo, now = new Date()) {
325
325
  ];
326
326
  const changed = fields.filter((field) => echo[field] !== envelope.approval[field]);
327
327
  if (Date.parse(envelope.approval.approvalExpiresAt) <= now.getTime()) {
328
- return { ok: false, blocker: "approval_expired", changed };
328
+ return {
329
+ ok: false,
330
+ blocker: "approval_expired",
331
+ changed,
332
+ };
329
333
  }
330
334
  return changed.length === 0
331
335
  ? { ok: true }
@@ -335,6 +339,85 @@ function validateApprovalEcho(envelope, echo, now = new Date()) {
335
339
  changed,
336
340
  };
337
341
  }
342
+ function blockedApprovalResult(params) {
343
+ return {
344
+ status: "blocked",
345
+ blocker: params.blocker,
346
+ guidance: "The durable refill approval no longer matches its persisted scope; render and approve a new packet before mutation.",
347
+ report: {
348
+ changed: params.changed,
349
+ ...(params.fresh === undefined ? {} : { fresh: params.fresh }),
350
+ },
351
+ readOnly: false,
352
+ forbiddenActions: FORBIDDEN_ACTIONS,
353
+ boundedAuthority: BOUNDED_AUTHORITY,
354
+ };
355
+ }
356
+ function storedApprovalFromStatus(value) {
357
+ const status = record(value) ?? {};
358
+ const config = record(status.config);
359
+ const scope = record(config?.scope);
360
+ const approval = record(config?.approval);
361
+ const scopeHash = text(config?.scopeHash);
362
+ if (!config || !scope || !approval || !scopeHash)
363
+ return null;
364
+ return {
365
+ config,
366
+ scope,
367
+ scopeHash,
368
+ approval,
369
+ hasDispatched: status.hasDispatched === true,
370
+ };
371
+ }
372
+ function validateStoredApprovalEcho(params) {
373
+ const fields = [
374
+ "scopeHash",
375
+ "targetShapeRevision",
376
+ "actionFingerprint",
377
+ "expectedTargetsHash",
378
+ "approvalExpiresAt",
379
+ "approvalFingerprint",
380
+ ];
381
+ const changed = fields.filter((field) => text(params.approval[field]) !== params.echo[field]);
382
+ const expiresAt = text(params.approval.approvalExpiresAt);
383
+ if (!params.allowExpired &&
384
+ (!expiresAt ||
385
+ Date.parse(expiresAt) <= (params.now ?? new Date()).getTime())) {
386
+ return {
387
+ ok: false,
388
+ blocker: "approval_expired",
389
+ changed,
390
+ };
391
+ }
392
+ return changed.length === 0
393
+ ? { ok: true }
394
+ : {
395
+ ok: false,
396
+ blocker: "approval_fingerprint_mismatch",
397
+ changed,
398
+ };
399
+ }
400
+ function executionScopeFromStored(scope, fallback) {
401
+ const dateSelector = record(scope.dateSelector) ?? {};
402
+ const kind = text(dateSelector.kind);
403
+ const senderIds = list(scope.senderIds)
404
+ .map((value) => text(value))
405
+ .filter((value) => value != null);
406
+ const approvalMode = scope.approvalMode === "mark_ready" ? "mark_ready" : "approve";
407
+ return {
408
+ intent: text(scope.intent) ?? fallback.intent,
409
+ senderIds,
410
+ approvalMode,
411
+ ...(kind === "target_date" && text(dateSelector.targetDate)
412
+ ? { targetDate: text(dateSelector.targetDate) }
413
+ : kind === "until_date" && text(dateSelector.untilDate)
414
+ ? { untilDate: text(dateSelector.untilDate) }
415
+ : kind === "horizon_send_days" &&
416
+ typeof dateSelector.horizonSendDays === "number"
417
+ ? { horizonSendDays: dateSelector.horizonSendDays }
418
+ : {}),
419
+ };
420
+ }
338
421
  async function maybeAddLostFenceGuidance(result, input) {
339
422
  if (result.status !== "blocked" ||
340
423
  result.blocker !== "lease_lost" ||
@@ -365,7 +448,53 @@ export async function refillSendsV2Command(input) {
365
448
  let resolvedScope = input.scope;
366
449
  let resolvedScopeHash = input.scopeHash;
367
450
  let resolvedApproval = input.approval;
368
- if (input.targetPlan && input.approvalEcho) {
451
+ if (input.runId && input.approvalEcho) {
452
+ const status = await refillRunStatus({ workspaceId, runId: input.runId });
453
+ const stored = storedApprovalFromStatus(status);
454
+ if (!stored) {
455
+ return {
456
+ status: "blocked",
457
+ blocker: "approval_run_unavailable",
458
+ guidance: "The durable refill run no longer exposes a valid persisted approval packet.",
459
+ report: { status },
460
+ readOnly: false,
461
+ forbiddenActions: FORBIDDEN_ACTIONS,
462
+ boundedAuthority: BOUNDED_AUTHORITY,
463
+ };
464
+ }
465
+ const validation = validateStoredApprovalEcho({
466
+ approval: stored.approval,
467
+ echo: input.approvalEcho,
468
+ allowExpired: stored.hasDispatched,
469
+ });
470
+ if (!validation.ok) {
471
+ return blockedApprovalResult({
472
+ blocker: validation.blocker,
473
+ changed: validation.changed,
474
+ fresh: stored.approval,
475
+ });
476
+ }
477
+ const freshTargetShapeRevision = input.targetPlan
478
+ ? (text(input.targetPlan.targetShapeRevision) ??
479
+ text(record(input.targetPlan.target)?.targetShapeRevision))
480
+ : null;
481
+ const storedTargetShapeRevision = text(stored.approval.targetShapeRevision);
482
+ if (freshTargetShapeRevision &&
483
+ freshTargetShapeRevision !== storedTargetShapeRevision) {
484
+ return blockedApprovalResult({
485
+ blocker: "approval_fingerprint_mismatch",
486
+ changed: ["targetShapeRevision"],
487
+ fresh: {
488
+ storedTargetShapeRevision,
489
+ freshTargetShapeRevision,
490
+ },
491
+ });
492
+ }
493
+ resolvedScope = stored.scope;
494
+ resolvedScopeHash = stored.scopeHash;
495
+ resolvedApproval = stored.approval;
496
+ }
497
+ else if (input.targetPlan && input.approvalEcho) {
369
498
  const envelope = buildApprovalEnvelope({
370
499
  workspaceId,
371
500
  intent: input.intent,
@@ -379,15 +508,11 @@ export async function refillSendsV2Command(input) {
379
508
  });
380
509
  const validation = validateApprovalEcho(envelope, input.approvalEcho);
381
510
  if (!validation.ok) {
382
- return {
383
- status: "blocked",
511
+ return blockedApprovalResult({
384
512
  blocker: validation.blocker,
385
- guidance: "The durable refill approval no longer matches fresh scope; render and approve a new packet before mutation.",
386
- report: { changed: validation.changed, fresh: envelope.approval },
387
- readOnly: false,
388
- forbiddenActions: FORBIDDEN_ACTIONS,
389
- boundedAuthority: BOUNDED_AUTHORITY,
390
- };
513
+ changed: validation.changed,
514
+ fresh: envelope.approval,
515
+ });
391
516
  }
392
517
  resolvedScope = envelope.scope;
393
518
  resolvedScopeHash = envelope.scopeHash;
@@ -409,13 +534,18 @@ export async function refillSendsV2Command(input) {
409
534
  boundedAuthority: BOUNDED_AUTHORITY,
410
535
  };
411
536
  }
537
+ const executionScope = resolvedScope
538
+ ? executionScopeFromStored(resolvedScope, input)
539
+ : {
540
+ intent: input.intent,
541
+ senderIds: input.senderIds,
542
+ approvalMode: input.approvalMode ?? "approve",
543
+ ...date,
544
+ };
412
545
  const result = await runRefillV2Loop({
413
546
  workspaceId,
414
- intent: input.intent,
415
- senderIds: input.senderIds,
416
- approvalMode: input.approvalMode ?? "approve",
547
+ ...executionScope,
417
548
  resume: resumeHandle(input),
418
- ...date,
419
549
  scope: resolvedScope,
420
550
  scopeHash: resolvedScopeHash,
421
551
  approval: resolvedApproval,
@@ -725,6 +725,23 @@ export declare function executeRefillSendsCommand(input?: RefillSendsCommandInpu
725
725
  intent: RefillSendsIntent;
726
726
  } | {
727
727
  yoloExecution: {
728
+ selectedAction: Record<string, unknown> | null;
729
+ targetPlanReread: boolean;
730
+ status: "blocked";
731
+ blocker: "approval_expired" | "approval_fingerprint_mismatch";
732
+ guidance: string;
733
+ report: {
734
+ fresh?: {} | null | undefined;
735
+ changed: string[];
736
+ };
737
+ readOnly: boolean;
738
+ forbiddenActions: string[];
739
+ boundedAuthority: string;
740
+ enabled: boolean;
741
+ result?: undefined;
742
+ refusalReason?: undefined;
743
+ postActionFirstAction?: undefined;
744
+ } | {
728
745
  selectedAction: Record<string, unknown> | null;
729
746
  targetPlanReread: boolean;
730
747
  readOnly: boolean;
@@ -1,6 +1,6 @@
1
+ import { REFILL_CONTRACT_CACHE_VERSION, REFILL_CONTRACT_HASH, REFILL_CONTRACT_VERSION, renderRefillBootstrapGuidance, renderRefillSafetyGuidance, renderRefillTerminalGuidance, } from "../refill-contract.js";
1
2
  import { actionIds, classifyPaidInmailRefreshReceipt, collectPaidInmailRefreshActions, executeOneYoloPrimitive, firstGlobalAction, normalizeStrings, numberValue, recordValue, refreshPaidInmailCreditsWithRetry, stringValue, } from "./refill-executors.js";
2
3
  import { prepareRefillSendsV2Approval, refillSendsV2Command, } from "./refill-sends-v2.js";
3
- import { REFILL_CONTRACT_CACHE_VERSION, REFILL_CONTRACT_HASH, REFILL_CONTRACT_VERSION, renderRefillBootstrapGuidance, renderRefillSafetyGuidance, renderRefillTerminalGuidance, } from "../refill-contract.js";
4
4
  import { getRefillTargetPlan } from "./refill-target-plan.js";
5
5
  import { createWorkspaceContext, normalizeExplicitWorkspaceId, } from "./workspace-context.js";
6
6
  const REFILL_SEND_ACTION_TYPES = ["send_invite", "send_inmail_closed"];
@@ -189,7 +189,7 @@ export const refillSendsToolDefinitions = [
189
189
  },
190
190
  expectedActionKey: {
191
191
  type: "string",
192
- description: "Optional actionKey from the approved packet's first global action. A mismatch returns approval_scope_changed before any refresh or mutation.",
192
+ description: "Optional actual non-empty actionKey from the approved packet's first global action. Never infer it from type, toolName, or actionFingerprint. A mismatch returns approval_scope_changed before any refresh or mutation.",
193
193
  },
194
194
  runId: { type: "string" },
195
195
  fence: { type: "number" },
@@ -293,7 +293,7 @@ function buildRefillSendsCommand(input) {
293
293
  : `, horizonSendDays: ${horizonSendDays}`}, approvalMode: "${approvalMode}" }) before any import, prep, approval, start, or schedule-affecting action.`,
294
294
  "Render target.eligibleSenderLedger and target.senderRefillPlans before mutation. Preserve the coverage labels Need to prepare, Goal, Already sent, Scheduled, Ready and waiting to be scheduled, and Still need.",
295
295
  "Use target.globalActionQueue as the only cross-sender yolo queue: execute only target.globalActionQueue[0], one globally ranked primitive, then rerun get_refill_target_plan before choosing another action.",
296
- "When expectedTargetShapeRevision or expectedActionKey is supplied from an already rendered packet, compare both against the first fresh target plan before paid-credit refresh or any primitive. Return approval_scope_changed with the fresh packet on mismatch.",
296
+ "When expectedTargetShapeRevision or expectedActionKey is supplied from an already rendered packet, compare both against the first fresh target plan before paid-credit refresh or any primitive. Pass expectedActionKey only when the rendered action has an actual non-empty actionKey; never infer it from type, toolName, or actionFingerprint. Return approval_scope_changed with the fresh packet on mismatch.",
297
297
  "Read target.senderRefillPlans[].refillReceipt as the public ladder receipt: it carries the selected campaign/sender/lane summary, skippedRungs, existingRowFrontier, and any absolute wait.deadlineAt. Do not choose source/copy/fallback work until that receipt proves the earlier ready/prep/approval rungs are exhausted.",
298
298
  "If get_refill_target_plan returns status complete, report eligible sender ledger, target.senderRefillPlans, gross target, selected days, sent count, scheduled count, projected count, campaign ids, targetShapeRevision, and no-op proof without asking for approval.",
299
299
  "Refill target lanes are connection invites (send_invite), standalone paid InMails (send_inmail_closed), or unified Sales Nav cascades represented publicly as send_inmail_closed with campaign classification sales_nav_cascade. For a Sales Nav cascade, refill the selected campaign first; its sequence can route prospects to Open InMail, paid InMail while fresh credits are >= 5, or same-campaign connection fallback without asking for separate open/paid/connection campaigns. Do not count send_dm as horizon target capacity.",
@@ -563,8 +563,13 @@ export async function executeRefillSendsCommand(input = {}) {
563
563
  }
564
564
  const targetPlanInput = targetPlanInputFor(scopedInput);
565
565
  const targetPlanBeforePaidRefresh = await getRefillTargetPlan(targetPlanInput);
566
+ const durableResumeRequested = Boolean(yolo &&
567
+ workspaceId &&
568
+ scopedInput.runId &&
569
+ typeof scopedInput.fence === "number" &&
570
+ scopedInput.approvalFingerprint);
566
571
  const approvedPacket = approvedPacketComparison(scopedInput, targetPlanBeforePaidRefresh);
567
- if (approvedPacket.changed) {
572
+ if (!durableResumeRequested && approvedPacket.changed) {
568
573
  return {
569
574
  ...command,
570
575
  ok: false,
@@ -595,7 +600,8 @@ export async function executeRefillSendsCommand(input = {}) {
595
600
  }
596
601
  if (yolo &&
597
602
  workspaceId &&
598
- durableV2PlanEligible(targetPlanBeforePaidRefresh)) {
603
+ (durableResumeRequested ||
604
+ durableV2PlanEligible(targetPlanBeforePaidRefresh))) {
599
605
  const base = durableYoloBaseResult({
600
606
  command,
601
607
  targetPlan: targetPlanBeforePaidRefresh,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.558",
3
+ "version": "0.1.560",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code, Codex, and Hermes campaign workflows",
6
6
  "main": "dist/index.js",
@@ -101,6 +101,9 @@ Claude Code uses `/sellable:refill-sends`. Optional selectors are `--sender`,
101
101
  6. Invoke only the public durable entrypoint:
102
102
  `mcp__sellable__refill_sends({ workspaceId, targetDate, untilDate, horizonSendDays, senderIds, senderNames, actionTypes, yolo, executionMode, requireWorkspace:true })`.
103
103
  Preserve the exact scope and approval echo returned by the tool on resume.
104
+ Pass `expectedActionKey` only when the rendered first global action contains
105
+ an actual non-empty `actionKey`; never infer it from `type`, `toolName`, or
106
+ `actionFingerprint`.
104
107
 
105
108
  ## Operator packet
106
109