@sellable/mcp 0.1.734 → 0.1.736

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.
@@ -133,6 +133,40 @@ export type RefillV2LoopResult = {
133
133
  refillReporting?: Record<string, unknown>;
134
134
  runState?: Record<string, unknown>;
135
135
  };
136
+ interface LoopContext {
137
+ workspaceId: string;
138
+ runId: string;
139
+ fence: number;
140
+ runHandle: RefillRunHandleV1;
141
+ targetConfig: RefillTargetConfigV1 | null;
142
+ gate: RefillLoopGate;
143
+ gateSeq: number;
144
+ runState: Record<string, unknown>;
145
+ journalPath: string | null;
146
+ plan: Record<string, unknown> | null;
147
+ headAction: Record<string, unknown> | null;
148
+ lastFingerprint: Fingerprint | null;
149
+ recoveryPoint: Record<string, unknown> | null;
150
+ gateCycles: number;
151
+ sleptMs: number;
152
+ blockedContinuations: unknown[];
153
+ lanesStarted: string[];
154
+ lanesTouched: string[];
155
+ invocationStartedAtMs: number;
156
+ agentKeywordsConsumed?: boolean;
157
+ }
158
+ interface Fingerprint {
159
+ planRevision: string | null;
160
+ stateRevision: string | null;
161
+ targetShapeRevision: string | null;
162
+ actionReadinessFingerprint: string | null;
163
+ }
164
+ declare function dominantSchedulerRefusalReason(receipt: Record<string, unknown> | null): {
165
+ source: string;
166
+ reason: string;
167
+ count: number;
168
+ } | null;
169
+ declare function schedulerSweepNoOpBlocker(ctx: LoopContext, reportInput: Record<string, unknown>): Record<string, unknown> | null;
136
170
  export declare function packetCoherenceFailure(plan: Record<string, unknown>): string | null;
137
171
  export declare function terminalProjection(value: {
138
172
  targetConfig?: unknown;
@@ -147,3 +181,8 @@ export declare function terminalProjection(value: {
147
181
  targetConfig?: RefillTargetConfigV1 | undefined;
148
182
  };
149
183
  export declare function runRefillV2Loop(input: RefillV2LoopInput, deps: RefillV2LoopDeps): Promise<RefillV2LoopResult>;
184
+ export declare const refillRunLoopInternals: {
185
+ schedulerSweepNoOpBlocker: typeof schedulerSweepNoOpBlocker;
186
+ dominantSchedulerRefusalReason: typeof dominantSchedulerRefusalReason;
187
+ };
188
+ export {};
@@ -405,6 +405,72 @@ function schedulerRunReceiptIsFreshZeroScheduled(schedulerRunReceipt) {
405
405
  return (effectiveStatus === "window_closed_noop" ||
406
406
  numberValue(receipt?.cellsScheduled) === 0);
407
407
  }
408
+ // fix(112al): the receipt's own dominant refusal reason, preferred in the order
409
+ // the scheduler applies its gates: prefilters drop ready cells before allocation
410
+ // (stale/invalid paid-InMail credit facts land here), skips are hard gate
411
+ // rejections of considered cells, defers are window/capacity/cooldown waits.
412
+ function dominantSchedulerRefusalReason(receipt) {
413
+ const sources = [
414
+ ["prefilterReasons", receipt?.prefilterReasons],
415
+ ["skippedReasons", receipt?.skippedReasons],
416
+ ["deferredReasons", receipt?.deferredReasons],
417
+ ];
418
+ for (const [source, value] of sources) {
419
+ const counts = recordValue(value);
420
+ if (!counts)
421
+ continue;
422
+ let best = null;
423
+ for (const [reason, rawCount] of Object.entries(counts)) {
424
+ const count = numberValue(rawCount) ?? 0;
425
+ if (count <= 0)
426
+ continue;
427
+ if (!best || count > best.count)
428
+ best = { reason, count };
429
+ }
430
+ if (best)
431
+ return { source, reason: best.reason, count: best.count };
432
+ }
433
+ return null;
434
+ }
435
+ // fix(112al): a sweep whose own receipt proves it placed zero while ready
436
+ // inventory existed. Gated on the executed action actually being a scheduler
437
+ // sweep and on a FRESH terminal receipt (ran / window_closed_noop), so an
438
+ // attached/backoff envelope with no outcome of its own never manufactures a
439
+ // blocker. readyCellsFound > 0 is required: a sweep that found no ready supply
440
+ // placed zero honestly and still completes.
441
+ function schedulerSweepNoOpBlocker(ctx, reportInput) {
442
+ const completedType = stringValue(reportInput.completedActionType) ??
443
+ stringValue(reportInput.nextActionType) ??
444
+ stringValue(recordValue(ctx.runState.lastExecutedAction)?.type);
445
+ if (completedType !== "run_scheduler_sweep")
446
+ return null;
447
+ const envelope = recordValue(runStateProgress(ctx).schedulerRunReceipt) ?? null;
448
+ if (!schedulerRunReceiptIsFreshZeroScheduled(envelope))
449
+ return null;
450
+ const receipt = recordValue(envelope?.receipt) ?? envelope;
451
+ const readyCellsFound = numberValue(receipt?.readyCellsFound) ?? 0;
452
+ if (readyCellsFound <= 0)
453
+ return null;
454
+ const dominant = dominantSchedulerRefusalReason(receipt);
455
+ return {
456
+ readyCellsFound,
457
+ cellsScheduled: numberValue(receipt?.cellsScheduled) ?? 0,
458
+ cellsConsidered: numberValue(receipt?.cellsConsidered) ?? 0,
459
+ ...(dominant
460
+ ? {
461
+ dominantReason: dominant.reason,
462
+ dominantReasonSource: dominant.source,
463
+ dominantReasonCount: dominant.count,
464
+ }
465
+ : {}),
466
+ ...(stringValue(receipt?.nextAction)
467
+ ? { recommendedAction: stringValue(receipt?.nextAction) }
468
+ : {}),
469
+ guidance: "The scheduler sweep executed and placed zero cells while ready inventory was present. " +
470
+ "Report the named reason above rather than re-running the identical sweep; re-running " +
471
+ "cannot place these rows until that gate clears.",
472
+ };
473
+ }
408
474
  // The settled nested receipt of a scheduler status read or a persisted
409
475
  // scheduler-run envelope: a receipt whose status is a terminal run outcome
410
476
  // (ran/window_closed_noop/failed). A status read nests it under `.receipt`; a
@@ -2205,7 +2271,31 @@ async function gateBootstrap(input, deps, ctx) {
2205
2271
  return null;
2206
2272
  }
2207
2273
  async function completeTerminal(input, deps, ctx, doneReasonInput, reportInput = {}) {
2208
- const doneReason = normalizeDoneReason(doneReasonInput);
2274
+ // fix(112al, intent-vs-effect): a scheduler sweep that placed NOTHING while
2275
+ // ready inventory existed must never report exact_action_complete. Dotwork
2276
+ // 2026-07-29 ran 68 consecutive refill_sends calls in which every executed
2277
+ // sweep returned exact_action_complete with cellsScheduled 0 against 20 ready
2278
+ // rows; the coordinator advanced next_exact_target and re-issued, and the
2279
+ // concrete blocker — paid_inmail_credit_receipt_invalid, present 460 times in
2280
+ // that run's own receipts — stayed invisible for the entire hour. The receipt
2281
+ // already carried the answer; nothing read it. Convert the terminal to the
2282
+ // documented blocked_retryable class and NAME the dominant receipt reason so
2283
+ // the next run reports the blocker instead of re-running the same no-op. The
2284
+ // scheduler action family is already deferred for the command by
2285
+ // coordinatorDeferredBlocker, so this only changes truthfulness, not routing.
2286
+ const sweepNoOp = normalizeDoneReason(doneReasonInput) === "exact_action_complete"
2287
+ ? schedulerSweepNoOpBlocker(ctx, reportInput)
2288
+ : null;
2289
+ const doneReason = sweepNoOp
2290
+ ? "blocked"
2291
+ : normalizeDoneReason(doneReasonInput);
2292
+ if (sweepNoOp) {
2293
+ reportInput = {
2294
+ ...reportInput,
2295
+ blocker: "scheduler_placed_zero_with_ready_supply",
2296
+ schedulerBlocker: sweepNoOp,
2297
+ };
2298
+ }
2209
2299
  // A lanes-exhausted terminal can still hold staged ready inventory whose
2210
2300
  // window days were never swept: the JIT walk only runs on the
2211
2301
  // scheduler-wait path, and the exhausted path went straight to terminal
@@ -4709,3 +4799,11 @@ export async function runRefillV2Loop(input, deps) {
4709
4799
  return classifyCaughtError(error, ctx);
4710
4800
  }
4711
4801
  }
4802
+ // fix(112al): exported for the intent-vs-effect pins. The wiring into
4803
+ // completeTerminal is covered by the existing run-loop suites staying green;
4804
+ // these expose the receipt-reading decision itself so each branch of the
4805
+ // contract is pinned directly against real receipt shapes.
4806
+ export const refillRunLoopInternals = {
4807
+ schedulerSweepNoOpBlocker,
4808
+ dominantSchedulerRefusalReason,
4809
+ };
@@ -1876,6 +1876,25 @@ export function sanitizeRefillTargetPlanResult(result) {
1876
1876
  const paidRefreshNeededSenderIdSet = paidRefreshNeededSenderIds(senderRefillPlans);
1877
1877
  const blockers = sanitizeBlockers(result.blockers, selectedKeys, paidRefreshNeededSenderIdSet);
1878
1878
  const request = isRecord(result.request) ? result.request : {};
1879
+ // fix(112ak-s2): this is the CLIENT mirror of the server's dated-sweep gate,
1880
+ // and it silently discarded the server's valid sweep. sanitizeTargetSchedulerSweep
1881
+ // derives its expected senders/actions from this map and returns null when the
1882
+ // map is empty, so a stricter rule here vetoes the server outright: Dotwork
1883
+ // 2026-07-29 (five lanes, remainingProjectedGap 10, readyBuffer 4, rrpg 6) had
1884
+ // the server emit a 20-placement sweep across all five senders, this loop
1885
+ // marked zero lanes eligible, and every served plan came back with
1886
+ // approve_messages at globalActionQueue[0] and no sweep at any rank. Because
1887
+ // the coordinator consumes the sanitized plan, that blocked execution, not just
1888
+ // display. 112ak-s relaxed the same predicate server-side
1889
+ // (refill-target-plan.ts buildExactDateSchedulerSweepAction): a lane earns a
1890
+ // sweep by HOLDING ready rows against an open dated slot, not by its ready
1891
+ // buffer fully covering its gap. No target-date condition is needed to mirror
1892
+ // the server's scoping — sanitizeTargetSchedulerSweep already refuses to run
1893
+ // without an expectedTargetDate matching ^\d{4}-\d{2}-\d{2}$, so this path is
1894
+ // explicit-date-only by construction. Deliberately the ONLY relaxed occurrence:
1895
+ // the wait_for_scheduler append and the status derivation below both keep
1896
+ // `remainingReadyOrProjectedGap === 0`, where full ready coverage is the
1897
+ // correct and intended meaning.
1879
1898
  const sweepEligibleBySender = new Map();
1880
1899
  for (const plan of senderPlans) {
1881
1900
  const senderId = stringValue(plan.senderId);
@@ -1883,7 +1902,6 @@ export function sanitizeRefillTargetPlanResult(result) {
1883
1902
  if (senderId &&
1884
1903
  actionType &&
1885
1904
  numberValue(plan.remainingProjectedGap) > 0 &&
1886
- numberValue(plan.remainingReadyOrProjectedGap) === 0 &&
1887
1905
  numberValue(plan.readyBuffer) > 0) {
1888
1906
  const lanes = sweepEligibleBySender.get(senderId) ?? new Set();
1889
1907
  lanes.add(actionType);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.734",
3
+ "version": "0.1.736",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code, Codex, and Hermes campaign workflows",
6
6
  "main": "dist/index.js",