@sellable/mcp 0.1.958 → 0.1.959

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.
@@ -35,7 +35,7 @@ export const REFILL_V3_ADVANCE_TOOL_NAME = "refill_v3_advance";
35
35
  export const refillV3AdvanceToolDefinitions = [
36
36
  {
37
37
  name: REFILL_V3_ADVANCE_TOOL_NAME,
38
- description: "THE DEFAULT REFILL ENTRY POINT. Whenever this tool is exposed, every ordinary request to refill, top up, fill, max out, load up, or otherwise add sends for a workspace, a sender, or a day routes HERE, and `refill_sends` is the superseded V1 fallback that applies only to servers which do NOT expose this tool. Never infer the route: use tool availability. When the operator names a workspace, resolve it with `list_workspaces`; fail closed if no accessible workspace matches or more than one matches, and never mutate the active workspace. Use configured context only when the operator named none. Start here: do not open the refill ladder with `get_refill_target_plan` or `refill_sends`. Advance exactly ONE fresh, fenced Refill V3 step for one workspace and scope. Pass only {workspaceId, scope}; refill defaults to yolo:true. YOLO is an autonomy grant, not a mode. Pass explicit yolo:false only when the operator asks for a conservative, review-first run. MCP carries signed run continuity internally. An advanced result is ALREADY EXECUTED and has reread its authority. For campaign_attention_required, call refill_v3_continue first with no decision: live-job waiting, exact actionable enrichment, and same-template generation are deterministic. Only model_decision_required routes the returned accurate campaign packet to refill-sends-work-campaign for one repair, criteria, rubric, waterfall, or source-expansion judgment. Repeat until complete, awaiting_external_change, or blocked, and report every terminal sender/date/lane row.",
38
+ description: "THE DEFAULT REFILL ENTRY POINT. Whenever this tool is exposed, every ordinary request to refill, top up, fill, max out, load up, or otherwise add sends for a workspace, a sender, or a day routes HERE, and `refill_sends` is the superseded V1 fallback that applies only to servers which do NOT expose this tool. Never infer the route: use tool availability. When the operator names a workspace, resolve it with `list_workspaces`; fail closed if no accessible workspace matches or more than one matches, and never mutate the active workspace. Use configured context only when the operator named none. Start here: do not open the refill ladder with `get_refill_target_plan` or `refill_sends`. Advance exactly ONE fresh, fenced Refill V3 step for one workspace and scope. Pass only {workspaceId, scope}; refill defaults to yolo:true. YOLO is an autonomy grant, not a mode. Pass explicit yolo:false only when the operator asks for a conservative, review-first run. MCP carries signed run continuity internally. An advanced result is ALREADY EXECUTED and has reread its authority. For campaign_attention_required, call refill_v3_continue first with no decision: live-job waiting, exact actionable enrichment, and same-template generation are deterministic. Only model_decision_required routes the returned accurate campaign packet to refill-sends-work-campaign for one repair, criteria, rubric, waterfall, or source-expansion judgment. Repeat until complete, awaiting_external_change, or blocked, and report every terminal sender/date/lane row. A thrown error is not a terminal: call this tool once more with the identical workspaceId and scope (the MCP keeps the run current) and report a blocker only if it fails the same way.",
39
39
  inputSchema: REFILL_V3_ADVANCE_INPUT_SCHEMA,
40
40
  outputSchema: REFILL_V3_ADVANCE_OUTPUT_SCHEMA,
41
41
  annotations: {
@@ -249,6 +249,12 @@ export type RefillV3AuthorityReceipt = {
249
249
  */
250
250
  externalReasonCode?: "endpoint_temporarily_unavailable";
251
251
  correctiveContext?: RefillV3AttentionCorrection;
252
+ /**
253
+ * Present only when the run's source-cycle bookkeeping for an effect that
254
+ * already happened could not be written. The effect and this receipt stand;
255
+ * the fresh re-observation decides the next step.
256
+ */
257
+ sourceCycleRecordError?: string;
252
258
  /**
253
259
  * V31-BATCH-001: the deterministic enrichment batch ladder's decision and
254
260
  * its numeric basis, present only on `enrich_bounded_sample` receipts so
@@ -270,6 +270,10 @@ export declare const REFILL_V3_CONTINUE_OUTPUT_SCHEMA: Readonly<{
270
270
  readonly receiptPersistence: {
271
271
  readonly type: "object";
272
272
  };
273
+ readonly recoveredFromError: {
274
+ readonly type: "string";
275
+ readonly minLength: 1;
276
+ };
273
277
  };
274
278
  readonly additionalProperties: false;
275
279
  readonly allOf: readonly [{
@@ -142,6 +142,9 @@ export const REFILL_V3_CONTINUE_OUTPUT_SCHEMA = Object.freeze({
142
142
  // consumed a sequence, so the caller can continue the SAME run.
143
143
  runToken: { type: "string", minLength: 1 },
144
144
  receiptPersistence: { type: "object" },
145
+ // The work's error when its one re-observation found the world had moved;
146
+ // the result is that fresh observation, handled by its own kind.
147
+ recoveredFromError: { type: "string", minLength: 1 },
145
148
  },
146
149
  additionalProperties: false,
147
150
  allOf: [
@@ -265,6 +265,10 @@ export declare const refillV3ContinueToolDefinitions: {
265
265
  readonly receiptPersistence: {
266
266
  readonly type: "object";
267
267
  };
268
+ readonly recoveredFromError: {
269
+ readonly type: "string";
270
+ readonly minLength: 1;
271
+ };
268
272
  };
269
273
  readonly additionalProperties: false;
270
274
  readonly allOf: readonly [{
@@ -390,31 +394,38 @@ export type RefillV3ContinueInput = {
390
394
  */
391
395
  decision?: unknown;
392
396
  };
397
+ /**
398
+ * Present when this continuation's work failed and its one re-observation
399
+ * found the world had moved; the result is that fresh observation.
400
+ */
401
+ type RefillV3RecoveredFromError = {
402
+ recoveredFromError?: string;
403
+ };
393
404
  export type RefillV3ContinueResult = {
394
405
  kind: "continuation_pending";
395
406
  retryAfterMs: number;
396
407
  runningForSeconds?: number;
397
- } | {
408
+ } | ({
398
409
  kind: "world_advanced";
399
410
  advanceResult: Extract<RefillV3AdvanceResult, {
400
411
  kind: "advanced";
401
412
  }>;
402
- } | {
413
+ } & RefillV3RecoveredFromError) | ({
403
414
  kind: "world_step_pending";
404
415
  advanceResult: Extract<RefillV3AdvanceResult, {
405
416
  kind: "run_step_pending";
406
417
  }>;
407
- } | {
418
+ } & RefillV3RecoveredFromError) | ({
408
419
  kind: "world_terminal";
409
420
  advanceResult: Extract<RefillV3AdvanceResult, {
410
421
  kind: "awaiting_external_change" | "complete" | "blocked";
411
422
  }>;
412
- } | {
423
+ } & RefillV3RecoveredFromError) | ({
413
424
  kind: "attention_refresh_required";
414
425
  advanceResult: Extract<RefillV3AdvanceResult, {
415
426
  kind: "campaign_attention_required";
416
427
  }>;
417
- } | {
428
+ } & RefillV3RecoveredFromError) | {
418
429
  kind: "model_decision_required";
419
430
  runToken: string;
420
431
  packet: RefillV3CampaignAttentionPacket;
@@ -566,6 +577,8 @@ type RefillV3ContinueProductionOwners = {
566
577
  protectedFitDigest: string;
567
578
  receipt: Record<string, unknown>;
568
579
  }) => Promise<unknown>;
580
+ /** Injectable pause for the bounded transient retry of a source-cycle write. */
581
+ sleep?: (ms: number) => Promise<void>;
569
582
  };
570
583
  export declare function runRefillV3CampaignWorkWithProductionOwners(input: {
571
584
  packet: RefillV3CampaignAttentionPacket;
@@ -18,7 +18,7 @@ export const REFILL_V3_CONTINUE_TOOL_NAME = "refill_v3_continue";
18
18
  export const refillV3ContinueToolDefinitions = [
19
19
  {
20
20
  name: REFILL_V3_CONTINUE_TOOL_NAME,
21
- description: "Continue exactly one server-issued Refill V3 campaign-attention packet. continuation_pending means the same operation is still running: wait retryAfterMs and call this tool with identical scope and no decision until it returns. Reconcile transport timeouts the same way; do not call advance or dispatch another search while pending. Call first without decision: after a fresh revalidation, the tool deterministically owns live-job waiting, selected-source row copying, exact actionable enrichment, and same-template message generation. It returns model_decision_required only when the accurate packet needs a genuine repair, criteria, rubric, waterfall, or source-expansion judgment; then call again with that one closed decision. Only an exact fresh packet can dispatch one existing campaign-work authority, which re-observes the exact sender/date and settles before a sibling campaign may advance. The caller cannot supply batch sizes, readiness digests, retry counters, fill inputs, scheduler inputs, or idempotency keys.",
21
+ description: "Continue exactly one server-issued Refill V3 campaign-attention packet. continuation_pending means the same operation is still running: wait retryAfterMs and call this tool with identical scope and no decision until it returns. Reconcile transport timeouts the same way; do not call advance or dispatch another search while pending. Call first without decision: after a fresh revalidation, the tool deterministically owns live-job waiting, selected-source row copying, exact actionable enrichment, and same-template message generation. It returns model_decision_required only when the accurate packet needs a genuine repair, criteria, rubric, waterfall, or source-expansion judgment; then call again with that one closed decision. Only an exact fresh packet can dispatch one existing campaign-work authority, which re-observes the exact sender/date and settles before a sibling campaign may advance. The caller cannot supply batch sizes, readiness digests, retry counters, fill inputs, scheduler inputs, or idempotency keys. A result with recoveredFromError already re-observed after a failure: handle it by its kind. A thrown error is not a terminal: call refill_v3_advance once more with the identical scope and report a blocker only if it fails the same way.",
22
22
  inputSchema: REFILL_V3_CONTINUE_INPUT_SCHEMA,
23
23
  outputSchema: REFILL_V3_CONTINUE_OUTPUT_SCHEMA,
24
24
  annotations: {
@@ -183,6 +183,33 @@ const DEFAULT_PRODUCTION_OWNERS = {
183
183
  export async function runRefillV3CampaignWorkWithProductionOwners(input, owners = DEFAULT_PRODUCTION_OWNERS) {
184
184
  const decoded = decodeRefillV3CampaignWorkOutput(input.output);
185
185
  const chosen = decoded.ok ? decoded.value.actions[0] : null;
186
+ // A source-cycle row is the run's BOOKKEEPING for a source effect that has
187
+ // already happened or been refused; it is never the effect itself. Throwing
188
+ // here on 2026-10-05 discarded Justin's applied 396-row engager import and
189
+ // stopped the Slack run with nothing scheduled. A transient failure is
190
+ // retried in place (retrying the whole stage would re-dispatch the search);
191
+ // any other failure rides on the receipt, and the persisted campaign-work
192
+ // receipt plus the fresh re-observation still guard against a repeat.
193
+ const recordSourceCycle = async (record) => {
194
+ if (!owners.recordSourceCycle)
195
+ return undefined;
196
+ const sleep = owners.sleep ??
197
+ ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
198
+ for (let attempt = 0;; attempt += 1) {
199
+ try {
200
+ await owners.recordSourceCycle(record);
201
+ return undefined;
202
+ }
203
+ catch (error) {
204
+ const retryDelay = POST_ADVANCE_TRANSPORT_RETRY_DELAYS_MS[attempt];
205
+ if (retryDelay === undefined ||
206
+ !isRetryablePostAdvanceTransportError(error)) {
207
+ return error instanceof Error ? error.message : String(error);
208
+ }
209
+ await sleep(retryDelay);
210
+ }
211
+ }
212
+ };
186
213
  let diagnosticRows = [];
187
214
  const record = (value) => value && typeof value === "object" && !Array.isArray(value)
188
215
  ? value
@@ -195,6 +222,9 @@ export async function runRefillV3CampaignWorkWithProductionOwners(input, owners
195
222
  const scopeEcho = `${input.packet.scope.workspaceId}:${input.packet.scope.campaignId}:${input.packet.scope.tableId}`;
196
223
  const receipt = (params) => ({
197
224
  ...(params.batchPlan === undefined ? {} : { batchPlan: params.batchPlan }),
225
+ ...(params.sourceCycleRecordError === undefined
226
+ ? {}
227
+ : { sourceCycleRecordError: params.sourceCycleRecordError }),
198
228
  outcome: params.outcome,
199
229
  result: params.result,
200
230
  scopeEcho,
@@ -813,7 +843,10 @@ export async function runRefillV3CampaignWorkWithProductionOwners(input, owners
813
843
  senderId: request.scope.senderId,
814
844
  date: request.scope.date,
815
845
  lane: input.packet.scope.lane,
816
- family: input.packet.evidence.leadSourceFamily,
846
+ // The family being SEARCHED, which a family switch makes differ from
847
+ // the packet's observed family. Its negative set is the one that
848
+ // guards this dispatch, and the receipt below names the same family.
849
+ family: request.sourceCommand.family,
817
850
  protectedFitDigest,
818
851
  };
819
852
  // The run fact scopes both sides of reconciliation: writes are stamped
@@ -884,28 +917,27 @@ export async function runRefillV3CampaignWorkWithProductionOwners(input, owners
884
917
  : null,
885
918
  });
886
919
  if (sourceOutcome.kind === "dispatched") {
887
- if (owners.recordSourceCycle) {
888
- await owners.recordSourceCycle({
889
- ...sourceCycleScope,
890
- ...sourceCycleRunFact,
891
- receipt: {
892
- schemaVersion: 1,
893
- kind: "search",
894
- campaignId: request.scope.campaignId,
895
- family: request.sourceCommand.family,
896
- protectedFitDigest,
897
- requestFingerprint: request.sourceCommand.requestFingerprint,
898
- sourceFingerprint: sourceOutcome.receipt.sourceAfterFingerprint,
899
- keywordBatch: request.sourceCommand.keywordBatch,
900
- postIds: request.sourceCommand.selectedPostIds,
901
- importedCount: sourceOutcome.receipt.importedCount,
902
- yieldEvidence: sourceOutcome.receipt.yieldEvidence,
903
- observedAt: request.scope.date,
904
- },
905
- });
906
- }
920
+ const sourceCycleRecordError = await recordSourceCycle({
921
+ ...sourceCycleScope,
922
+ ...sourceCycleRunFact,
923
+ receipt: {
924
+ schemaVersion: 1,
925
+ kind: "search",
926
+ campaignId: request.scope.campaignId,
927
+ family: request.sourceCommand.family,
928
+ protectedFitDigest,
929
+ requestFingerprint: request.sourceCommand.requestFingerprint,
930
+ sourceFingerprint: sourceOutcome.receipt.sourceAfterFingerprint,
931
+ keywordBatch: request.sourceCommand.keywordBatch,
932
+ postIds: request.sourceCommand.selectedPostIds,
933
+ importedCount: sourceOutcome.receipt.importedCount,
934
+ yieldEvidence: sourceOutcome.receipt.yieldEvidence,
935
+ observedAt: request.scope.date,
936
+ },
937
+ });
907
938
  const changed = sourceOutcome.receipt.importedCount > 0;
908
939
  return receipt({
940
+ sourceCycleRecordError,
909
941
  outcome: changed ? "applied" : "no_effect",
910
942
  result,
911
943
  idempotencyKey: request.idempotencyKey,
@@ -914,27 +946,26 @@ export async function runRefillV3CampaignWorkWithProductionOwners(input, owners
914
946
  });
915
947
  }
916
948
  if (sourceOutcome.kind === "recorded_exhaustion") {
917
- if (owners.recordSourceCycle) {
918
- await owners.recordSourceCycle({
919
- ...sourceCycleScope,
920
- ...sourceCycleRunFact,
921
- receipt: {
922
- schemaVersion: 1,
923
- kind: "exhaustion",
924
- campaignId: request.scope.campaignId,
925
- family: request.sourceCommand.family,
926
- protectedFitDigest,
927
- requestFingerprint: request.idempotencyKey,
928
- sourceFingerprint: priorSourceFingerprint ?? request.scope.evidenceFingerprint,
929
- keywordBatch: null,
930
- postIds: null,
931
- importedCount: 0,
932
- yieldEvidence: sourceOutcome.receipt.yieldEvidence,
933
- observedAt: request.scope.date,
934
- },
935
- });
936
- }
949
+ const sourceCycleRecordError = await recordSourceCycle({
950
+ ...sourceCycleScope,
951
+ ...sourceCycleRunFact,
952
+ receipt: {
953
+ schemaVersion: 1,
954
+ kind: "exhaustion",
955
+ campaignId: request.scope.campaignId,
956
+ family: request.sourceCommand.family,
957
+ protectedFitDigest,
958
+ requestFingerprint: request.idempotencyKey,
959
+ sourceFingerprint: priorSourceFingerprint ?? request.scope.evidenceFingerprint,
960
+ keywordBatch: null,
961
+ postIds: null,
962
+ importedCount: 0,
963
+ yieldEvidence: sourceOutcome.receipt.yieldEvidence,
964
+ observedAt: request.scope.date,
965
+ },
966
+ });
937
967
  return receipt({
968
+ sourceCycleRecordError,
938
969
  outcome: "no_effect",
939
970
  result,
940
971
  idempotencyKey: request.idempotencyKey,
@@ -949,7 +980,7 @@ export async function runRefillV3CampaignWorkWithProductionOwners(input, owners
949
980
  // returning the retained job wait. Previously the pending branch
950
981
  // wrote nothing, so the job could settle and disappear while the
951
982
  // next advance still advertised the identical round as untried.
952
- await owners.recordSourceCycle({
983
+ const sourceCycleRecordError = await recordSourceCycle({
953
984
  ...sourceCycleScope,
954
985
  ...sourceCycleRunFact,
955
986
  receipt: {
@@ -975,6 +1006,7 @@ export async function runRefillV3CampaignWorkWithProductionOwners(input, owners
975
1006
  // evidence-free timeout: reobserve so the next coordinator step can
976
1007
  // wait on (or consume) the exact job without replaying the search.
977
1008
  return receipt({
1009
+ sourceCycleRecordError,
978
1010
  outcome: "no_effect",
979
1011
  result,
980
1012
  idempotencyKey: request.idempotencyKey,
@@ -995,27 +1027,26 @@ export async function runRefillV3CampaignWorkWithProductionOwners(input, owners
995
1027
  // authority, and let the same outer run re-observe/continue. Returning
996
1028
  // `receipt_incomplete` here left settlement null and repeated the
997
1029
  // same packet while every sibling sender/lane stayed unreachable.
998
- if (owners.recordSourceCycle) {
999
- await owners.recordSourceCycle({
1000
- ...sourceCycleScope,
1001
- ...sourceCycleRunFact,
1002
- receipt: {
1003
- schemaVersion: 1,
1004
- kind: "exhaustion",
1005
- campaignId: request.scope.campaignId,
1006
- family: request.sourceCommand.family,
1007
- protectedFitDigest,
1008
- requestFingerprint: request.idempotencyKey,
1009
- sourceFingerprint: priorSourceFingerprint ?? request.scope.evidenceFingerprint,
1010
- keywordBatch: null,
1011
- postIds: null,
1012
- importedCount: 0,
1013
- yieldEvidence: input.packet.evidence.priorYieldReceipt,
1014
- observedAt: request.scope.date,
1015
- },
1016
- });
1017
- }
1030
+ const sourceCycleRecordError = await recordSourceCycle({
1031
+ ...sourceCycleScope,
1032
+ ...sourceCycleRunFact,
1033
+ receipt: {
1034
+ schemaVersion: 1,
1035
+ kind: "exhaustion",
1036
+ campaignId: request.scope.campaignId,
1037
+ family: request.sourceCommand.family,
1038
+ protectedFitDigest,
1039
+ requestFingerprint: request.idempotencyKey,
1040
+ sourceFingerprint: priorSourceFingerprint ?? request.scope.evidenceFingerprint,
1041
+ keywordBatch: null,
1042
+ postIds: null,
1043
+ importedCount: 0,
1044
+ yieldEvidence: input.packet.evidence.priorYieldReceipt,
1045
+ observedAt: request.scope.date,
1046
+ },
1047
+ });
1018
1048
  return receipt({
1049
+ sourceCycleRecordError,
1019
1050
  outcome: "no_effect",
1020
1051
  result: "source_frontier_exhausted",
1021
1052
  idempotencyKey: request.idempotencyKey,
@@ -1059,28 +1090,26 @@ export async function runRefillV3CampaignWorkWithProductionOwners(input, owners
1059
1090
  yieldEvidence: input.packet.evidence.priorYieldReceipt,
1060
1091
  });
1061
1092
  if (exhaustionVerdict.exhausted) {
1062
- if (owners.recordSourceCycle) {
1063
- await owners.recordSourceCycle({
1064
- ...sourceCycleScope,
1065
- ...sourceCycleRunFact,
1066
- receipt: {
1067
- schemaVersion: 1,
1068
- kind: "exhaustion",
1069
- campaignId: request.scope.campaignId,
1070
- family: request.sourceCommand.family,
1071
- protectedFitDigest,
1072
- requestFingerprint: request.idempotencyKey,
1073
- sourceFingerprint: priorSourceFingerprint ??
1074
- request.scope.evidenceFingerprint,
1075
- keywordBatch: null,
1076
- postIds: null,
1077
- importedCount: 0,
1078
- yieldEvidence: input.packet.evidence.priorYieldReceipt,
1079
- observedAt: request.scope.date,
1080
- },
1081
- });
1082
- }
1093
+ const sourceCycleRecordError = await recordSourceCycle({
1094
+ ...sourceCycleScope,
1095
+ ...sourceCycleRunFact,
1096
+ receipt: {
1097
+ schemaVersion: 1,
1098
+ kind: "exhaustion",
1099
+ campaignId: request.scope.campaignId,
1100
+ family: request.sourceCommand.family,
1101
+ protectedFitDigest,
1102
+ requestFingerprint: request.idempotencyKey,
1103
+ sourceFingerprint: priorSourceFingerprint ?? request.scope.evidenceFingerprint,
1104
+ keywordBatch: null,
1105
+ postIds: null,
1106
+ importedCount: 0,
1107
+ yieldEvidence: input.packet.evidence.priorYieldReceipt,
1108
+ observedAt: request.scope.date,
1109
+ },
1110
+ });
1083
1111
  return receipt({
1112
+ sourceCycleRecordError,
1084
1113
  outcome: "no_effect",
1085
1114
  result: "source_frontier_exhausted",
1086
1115
  idempotencyKey: request.idempotencyKey,
@@ -1143,12 +1172,13 @@ export async function runRefillV3CampaignWorkWithProductionOwners(input, owners
1143
1172
  });
1144
1173
  }
1145
1174
  if (request.action === "record_source_frontier_exhausted") {
1175
+ let sourceCycleRecordError;
1146
1176
  if (owners.recordSourceCycle) {
1147
1177
  const protectedFitDigest = input.packet.evidence.protectedFitDigest ||
1148
1178
  `sha256:${createHash("sha256")
1149
1179
  .update(input.packet.evidence.campaignObjective)
1150
1180
  .digest("hex")}`;
1151
- await owners.recordSourceCycle({
1181
+ sourceCycleRecordError = await recordSourceCycle({
1152
1182
  workspaceId: request.scope.workspaceId,
1153
1183
  runToken: input.runToken,
1154
1184
  scopeFingerprint: input.scopeFingerprint,
@@ -1176,6 +1206,7 @@ export async function runRefillV3CampaignWorkWithProductionOwners(input, owners
1176
1206
  });
1177
1207
  }
1178
1208
  return receipt({
1209
+ sourceCycleRecordError,
1179
1210
  outcome: "no_effect",
1180
1211
  result,
1181
1212
  idempotencyKey: request.idempotencyKey,
@@ -1368,7 +1399,63 @@ function deterministicCampaignDecision(packet) {
1368
1399
  consumedDiagnosticRowIds: null,
1369
1400
  };
1370
1401
  }
1402
+ /**
1403
+ * V33-JUSTIN-SELF-HEAL · an unattended refill never ends on a raw error that
1404
+ * a fresh observation can answer.
1405
+ *
1406
+ * On 2026-10-05 Justin's own-post refresh imported 396 engagers and a
1407
+ * bookkeeping 409 then escaped as a raw error, so the Slack host reported a
1408
+ * blocker with nothing scheduled although the run credential was still
1409
+ * current. Once this continuation holds a refreshed credential, any failure
1410
+ * gets ONE re-observation with it. When the world moved, that observation is
1411
+ * returned like any other and names the error it recovered from. When nothing
1412
+ * moved, the same work would fail the same way, so the original error stands:
1413
+ * a deterministic failure surfaces instead of looping. A failure before the
1414
+ * credential was refreshed is never masked.
1415
+ */
1371
1416
  async function executeRefillV3Continuation(input, dependencies) {
1417
+ const progress = {
1418
+ runToken: null,
1419
+ packet: null,
1420
+ };
1421
+ try {
1422
+ return await executeRefillV3ContinuationStep(input, dependencies, progress);
1423
+ }
1424
+ catch (error) {
1425
+ const runToken = progress.runToken;
1426
+ const failedPacket = progress.packet;
1427
+ if (!runToken || !failedPacket)
1428
+ throw error;
1429
+ let observed;
1430
+ try {
1431
+ observed = await reconnectPostAdvanceStage({
1432
+ sleep: dependencies.sleep,
1433
+ operation: () => dependencies.advance({
1434
+ workspaceId: failedPacket.scope.workspaceId,
1435
+ scope: input.scope,
1436
+ ...(input.campaignId === undefined
1437
+ ? {}
1438
+ : { campaignId: input.campaignId }),
1439
+ ...(input.yolo === undefined ? {} : { yolo: input.yolo }),
1440
+ runToken,
1441
+ }),
1442
+ });
1443
+ }
1444
+ catch {
1445
+ throw error;
1446
+ }
1447
+ if (observed.kind === "campaign_attention_required" &&
1448
+ samePacket(failedPacket, observed.packet)) {
1449
+ throw error;
1450
+ }
1451
+ return {
1452
+ ...continuationResultFromAdvance(observed),
1453
+ recoveredFromError: (error instanceof Error ? error.message : String(error)) ||
1454
+ "unknown_error",
1455
+ };
1456
+ }
1457
+ }
1458
+ async function executeRefillV3ContinuationStep(input, dependencies, progress) {
1372
1459
  const workspaceId = normalizeExplicitWorkspaceId(input.workspaceId);
1373
1460
  if (!workspaceId || workspaceId !== input.packet.scope.workspaceId) {
1374
1461
  throw new Error("refill_v3_continue requires one exact workspace matching the server-issued packet.");
@@ -1422,6 +1509,8 @@ async function executeRefillV3Continuation(input, dependencies) {
1422
1509
  if (!runToken || !scopeFingerprint) {
1423
1510
  throw new Error("refill_v3_continue got no refreshed run credential from its own internal advance, which already consumed this run's sequence. Refusing to return a spent token.");
1424
1511
  }
1512
+ progress.runToken = runToken;
1513
+ progress.packet = advanceResult.packet;
1425
1514
  const deterministicDecision = deterministicCampaignDecision(advanceResult.packet);
1426
1515
  const decision = deterministicDecision ?? input.decision;
1427
1516
  if (decision === undefined) {
@@ -1642,6 +1731,9 @@ async function executeRefillV3Continuation(input, dependencies) {
1642
1731
  if (input.yolo === true &&
1643
1732
  (preparationOwnsSettlement || directlyQueuedRowsOwnSettlement) &&
1644
1733
  campaignSettlementIsReady(settlement)) {
1734
+ // This advance spends the refreshed credential, so a failure past here has
1735
+ // no unspent credential left to re-observe with.
1736
+ progress.runToken = null;
1645
1737
  const postSettlementAdvance = await reconnectPostAdvanceStage({
1646
1738
  sleep: dependencies.sleep,
1647
1739
  operation: () => dependencies.advance({
@@ -11516,6 +11516,10 @@ export declare const allTools: ({
11516
11516
  readonly receiptPersistence: {
11517
11517
  readonly type: "object";
11518
11518
  };
11519
+ readonly recoveredFromError: {
11520
+ readonly type: "string";
11521
+ readonly minLength: 1;
11522
+ };
11519
11523
  };
11520
11524
  readonly additionalProperties: false;
11521
11525
  readonly allOf: readonly [{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.958",
3
+ "version": "0.1.959",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code, Codex, and Hermes campaign workflows",
6
6
  "main": "dist/index.js",
@@ -823,6 +823,13 @@ process lives. If a server restart reports that retained attention is missing,
823
823
  stop with `continuation_reconciliation_required` and the exact scope and last
824
824
  receipt; do not invent a replacement run or claim the prior effect failed.
825
825
  Never treat a timeout as zero effect.
826
+ A result carrying `recoveredFromError` means the continuation's work failed,
827
+ the MCP re-observed once, and the world had moved: handle that result by its
828
+ own kind and name the error in the final report. A thrown error is not a
829
+ terminal: the MCP still holds the run's current credential, so call
830
+ `refill_v3_advance` once more with the identical workspace, scope, campaignId,
831
+ and yolo grant. Report a blocker only if that fresh call fails with the same
832
+ error.
826
833
  Continue the authorized refill across every connected sender and requested
827
834
  lane until the coordinator returns row-complete terminal evidence. Preserve the
828
835
  rubric, message, caps, and exclusions; pending work needs no renewed approval.
@@ -456,6 +456,13 @@ Each call answers with exactly one of six results:
456
456
  restarted MCP reports missing retained attention, stop with
457
457
  `continuation_reconciliation_required`, the exact scope, and the last receipt;
458
458
  do not open a replacement run for an uncertain effect.
459
+ A result carrying `recoveredFromError` means the continuation's work failed,
460
+ the MCP re-observed once, and the world had moved: handle that result by its
461
+ own kind and name the error in the final report. A thrown error is not a
462
+ terminal: the MCP still holds the run's current credential, so call
463
+ `refill_v3_advance` once more with the identical workspace, scope,
464
+ campaignId, and yolo grant. Report a blocker only if that fresh call fails
465
+ with the same error.
459
466
  Interpret its world wrappers
460
467
  by their outer kind: `world_terminal` means `advanceResult` is already the
461
468
  canonical `awaiting_external_change`, `complete`, or `blocked` terminal, so