@sellable/mcp 0.1.554 → 0.1.555

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.
Files changed (40) hide show
  1. package/dist/index-dev.js +0 -0
  2. package/dist/index.js +0 -0
  3. package/dist/refill-contract.d.ts +157 -0
  4. package/dist/refill-contract.js +487 -0
  5. package/dist/refill-run-client.d.ts +5 -0
  6. package/dist/refill-run-client.js +15 -0
  7. package/dist/refill-run-loop.d.ts +12 -1
  8. package/dist/refill-run-loop.js +158 -13
  9. package/dist/tools/campaign-message-preparation.d.ts +62 -0
  10. package/dist/tools/campaign-message-preparation.js +41 -0
  11. package/dist/tools/evergreen-refill-plan.d.ts +3 -0
  12. package/dist/tools/evergreen-refill-plan.js +29 -7
  13. package/dist/tools/prompts.js +9 -0
  14. package/dist/tools/refill-executors.d.ts +38 -0
  15. package/dist/tools/refill-executors.js +222 -3
  16. package/dist/tools/refill-sends-v2.d.ts +112 -1
  17. package/dist/tools/refill-sends-v2.js +302 -2
  18. package/dist/tools/refill-sends.d.ts +678 -32
  19. package/dist/tools/refill-sends.js +274 -13
  20. package/dist/tools/refill-target-plan.js +486 -14
  21. package/dist/tools/registry.d.ts +96 -27
  22. package/dist/tools/registry.js +1 -3
  23. package/dist/tools/scheduler-fill-capacity.js +1 -1
  24. package/dist/tools/scheduler-run.d.ts +71 -0
  25. package/dist/tools/scheduler-run.js +203 -1
  26. package/dist/tools/workspaces.d.ts +4 -6
  27. package/dist/tools/workspaces.js +11 -13
  28. package/package.json +1 -1
  29. package/skills/refill-sends/SKILL.md +91 -353
  30. package/skills/refill-sends-v2/SKILL.md +6 -6
  31. package/skills/refill-sends-v2-workflow/SKILL.md +5 -5
  32. package/skills/refill-sends-v2-workflow/core/flow.v1.json +8 -8
  33. package/skills/refill-sends-workflow/SKILL.md +100 -743
  34. package/skills/refill-sends-workflow/core/contract.v2.json +543 -0
  35. package/skills/refill-sends-workflow/core/flow.v1.json +185 -1
  36. package/dist/refill-date-window.d.ts +0 -34
  37. package/dist/refill-date-window.js +0 -210
  38. package/dist/tools/refill-sends-evergreen.d.ts +0 -28
  39. package/dist/tools/refill-sends-evergreen.js +0 -47
  40. package/skills/research/config.json +0 -9
@@ -1,6 +1,10 @@
1
1
  import { actionIds, classifyPaidInmailRefreshReceipt, collectPaidInmailRefreshActions, executeOneYoloPrimitive, firstGlobalAction, normalizeStrings, numberValue, recordValue, refreshPaidInmailCreditsWithRetry, stringValue, } from "./refill-executors.js";
2
+ 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";
2
4
  import { getRefillTargetPlan } from "./refill-target-plan.js";
3
5
  import { createWorkspaceContext, normalizeExplicitWorkspaceId, } from "./workspace-context.js";
6
+ const REFILL_SEND_ACTION_TYPES = ["send_invite", "send_inmail_closed"];
7
+ const REFILL_SEND_ACTION_TYPE_SET = new Set(REFILL_SEND_ACTION_TYPES);
4
8
  const DEFAULT_SCHEDULER_FORWARD_HOURS = 48;
5
9
  const MCP_PAID_INMAIL_CREDITS_MAX_STALENESS_SECONDS = 4 * 60 * 60;
6
10
  function normalizeHorizonSendDays(value) {
@@ -48,10 +52,60 @@ function normalizeTargetDate(value) {
48
52
  }
49
53
  return trimmed;
50
54
  }
55
+ function normalizeRefillActionTypes(value) {
56
+ if (value === undefined || value === null)
57
+ return undefined;
58
+ if (!Array.isArray(value)) {
59
+ throw new Error("actionTypes must be an array of supported refill lanes.");
60
+ }
61
+ if (value.some((actionType) => typeof actionType !== "string")) {
62
+ throw new Error(`Unsupported refill actionTypes. Supported values are ${REFILL_SEND_ACTION_TYPES.join(", ")}.`);
63
+ }
64
+ const normalized = normalizeStrings(value);
65
+ const unsupported = normalized.filter((actionType) => !REFILL_SEND_ACTION_TYPE_SET.has(actionType));
66
+ if (unsupported.length > 0) {
67
+ throw new Error(`Unsupported refill actionTypes: ${unsupported.join(", ")}. Supported values are ${REFILL_SEND_ACTION_TYPES.join(", ")}.`);
68
+ }
69
+ return normalized.length > 0
70
+ ? normalized
71
+ : undefined;
72
+ }
73
+ function normalizeExpectedPacketValue(value, field) {
74
+ if (value === undefined || value === null)
75
+ return undefined;
76
+ if (typeof value !== "string" || value.trim().length === 0) {
77
+ throw new Error(`${field} must be a non-empty string when provided.`);
78
+ }
79
+ return value.trim();
80
+ }
81
+ function normalizeRefillSendsInput(input) {
82
+ const actionTypes = normalizeRefillActionTypes(input.actionTypes);
83
+ const expectedTargetShapeRevision = normalizeExpectedPacketValue(input.expectedTargetShapeRevision, "expectedTargetShapeRevision");
84
+ const expectedActionKey = normalizeExpectedPacketValue(input.expectedActionKey, "expectedActionKey");
85
+ const normalized = { ...input };
86
+ delete normalized.actionTypes;
87
+ delete normalized.expectedTargetShapeRevision;
88
+ delete normalized.expectedActionKey;
89
+ return {
90
+ ...normalized,
91
+ ...(actionTypes ? { actionTypes } : {}),
92
+ ...(expectedTargetShapeRevision ? { expectedTargetShapeRevision } : {}),
93
+ ...(expectedActionKey ? { expectedActionKey } : {}),
94
+ };
95
+ }
51
96
  export const refillSendsToolDefinitions = [
52
97
  {
53
98
  name: "refill_sends",
54
- description: "Typed command entrypoint for Sellable refill sends. Accepts --yolo semantics and optional sender selectors, then returns the bounded execution contract for the skill-led refill workflow. In --yolo, it may refresh stale paid InMail credit cache facts once, or execute exactly one safe primitive from target.globalActionQueue[0] (bounded existing-row preparation, bounded generated-message approval, receipt-proven same-source copy, or read-only wait) and reread. Same-source copy/source fallback is only safe after receipt-proven exhaustion: hasMoreFrontierRows:false, zero approvalCandidates, no stuckActiveCells, and no non-terminal approvedNotDispatched work. Wait primitives are gates, not competing goals, and carry absolute wait.deadlineAt when present. It does not run unbounded approval, lower paid InMail thresholds, switch source families, create campaigns, schedule sends, launch, archive, delete, or write scheduler rows.",
99
+ description: [
100
+ "Public durable refill entrypoint. It preserves explicit workspace/date/selectors, renders the normal bounded packet even under yolo, executes one approved primitive, records a typed receipt, rereads, and replans.",
101
+ "An exact-date request-scoped run_scheduler_sweep may schedule eligible workspace cells through product gates; it never sends directly or raw-writes scheduler fields.",
102
+ `contractVersion=${REFILL_CONTRACT_VERSION}`,
103
+ `cacheVersion=${REFILL_CONTRACT_CACHE_VERSION}`,
104
+ `contractHash=${REFILL_CONTRACT_HASH}`,
105
+ renderRefillBootstrapGuidance(),
106
+ renderRefillSafetyGuidance(),
107
+ renderRefillTerminalGuidance(),
108
+ ].join(" "),
55
109
  inputSchema: {
56
110
  type: "object",
57
111
  properties: {
@@ -87,6 +141,14 @@ export const refillSendsToolDefinitions = [
87
141
  items: { type: "string" },
88
142
  description: "Optional sender display names to resolve through list_senders before campaign selection.",
89
143
  },
144
+ actionTypes: {
145
+ type: "array",
146
+ items: {
147
+ type: "string",
148
+ enum: ["send_invite", "send_inmail_closed"],
149
+ },
150
+ description: "Optional explicit refill lane. Omit to preserve planner inference; supported values are connection invites and paid InMail / unified Sales Nav cascades.",
151
+ },
90
152
  horizonSendDays: {
91
153
  type: "number",
92
154
  minimum: 1,
@@ -121,6 +183,22 @@ export const refillSendsToolDefinitions = [
121
183
  enum: ["approve", "mark_ready"],
122
184
  description: 'Preparation mode for the final packet. Use "approve" only when the user asked to fill/schedule sender sends; otherwise default to "mark_ready".',
123
185
  },
186
+ expectedTargetShapeRevision: {
187
+ type: "string",
188
+ description: "Optional targetShapeRevision from an already rendered and approved packet. A mismatch returns approval_scope_changed before any refresh or mutation.",
189
+ },
190
+ expectedActionKey: {
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.",
193
+ },
194
+ runId: { type: "string" },
195
+ fence: { type: "number" },
196
+ scopeHash: { type: "string" },
197
+ targetShapeRevision: { type: "string" },
198
+ actionFingerprint: { type: "string" },
199
+ expectedTargetsHash: { type: "string" },
200
+ approvalExpiresAt: { type: "string" },
201
+ approvalFingerprint: { type: "string" },
124
202
  },
125
203
  required: [],
126
204
  additionalProperties: false,
@@ -128,6 +206,9 @@ export const refillSendsToolDefinitions = [
128
206
  },
129
207
  ];
130
208
  export function refillSendsCommand(input = {}) {
209
+ return buildRefillSendsCommand(normalizeRefillSendsInput(input));
210
+ }
211
+ function buildRefillSendsCommand(input) {
131
212
  const workspaceId = normalizeExplicitWorkspaceId(input.workspaceId);
132
213
  const senders = normalizeStrings(input.senders);
133
214
  const senderIds = normalizeStrings(input.senderIds);
@@ -142,6 +223,7 @@ export function refillSendsCommand(input = {}) {
142
223
  : normalizeHorizonSendDays(input.horizonSendDays);
143
224
  const intent = input.intent ?? "plain";
144
225
  const approvalMode = input.approvalMode ?? (yolo ? "approve" : "mark_ready");
226
+ const actionTypes = input.actionTypes;
145
227
  const senderScope = hasSenderSelectors
146
228
  ? "selected_senders"
147
229
  : yolo
@@ -161,6 +243,7 @@ export function refillSendsCommand(input = {}) {
161
243
  workspaceId,
162
244
  workspaceResolution: workspaceId ? "explicit" : "active_config",
163
245
  intent,
246
+ ...(actionTypes ? { actionTypes } : {}),
164
247
  targetDate,
165
248
  untilDate,
166
249
  horizonSendDays,
@@ -201,7 +284,7 @@ export function refillSendsCommand(input = {}) {
201
284
  "A skill cannot create or invoke /goal by itself. If this refill is already running inside an active Codex goal, keep that goal open until every selected sender lane is horizon-filled by projected coverage (sent + scheduled), Christian explicitly stops/statuses the run, or a concrete non-scheduler blocker appears.",
202
285
  `Call get_refill_target_plan({ intent: "${intent}"${workspaceId ? `, workspaceId: "${workspaceId}"` : ""}${input.campaignId ? `, campaignId: "${input.campaignId}"` : ""}${input.tableId ? `, tableId: "${input.tableId}"` : ""}${senderIds.length > 0 ? `, senderIds: ${JSON.stringify(senderIds)}` : ""}${senderNames.length > 0
203
286
  ? `, senderNames: ${JSON.stringify(senderNames)}`
204
- : ""}${senders.length > 0 ? `, senders: ${JSON.stringify(senders)}` : ""}${targetDate
287
+ : ""}${senders.length > 0 ? `, senders: ${JSON.stringify(senders)}` : ""}${actionTypes ? `, actionTypes: ${JSON.stringify(actionTypes)}` : ""}${targetDate
205
288
  ? `, targetDate: "${targetDate}"`
206
289
  : untilDate
207
290
  ? `, untilDate: "${untilDate}"`
@@ -210,10 +293,11 @@ export function refillSendsCommand(input = {}) {
210
293
  : `, horizonSendDays: ${horizonSendDays}`}, approvalMode: "${approvalMode}" }) before any import, prep, approval, start, or schedule-affecting action.`,
211
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.",
212
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.",
213
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.",
214
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.",
215
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.",
216
- "If remainingReadyOrProjectedGap is 0 but remainingProjectedGap is positive, run only a persistent read-only scheduler wait/reread loop; do not ask for prep/import/approval and do not close out while scheduler pickup is the only remaining state.",
300
+ "If the exact-date global action is run_scheduler_sweep, execute it once and fully reread when remainingReadyOrProjectedGap is 0 but remainingProjectedGap is positive. Otherwise, run only a persistent read-only scheduler wait/reread loop; do not ask for prep/import/approval and do not close out while scheduler pickup is the only remaining state.",
217
301
  `Resolve route with resolve_campaign_fill_route({ intent: "${intent}"${workspaceId ? `, workspaceId: "${workspaceId}"` : ""}${input.campaignId ? `, campaignId: "${input.campaignId}"` : ""}${input.tableId ? `, tableId: "${input.tableId}"` : ""} }).`,
218
302
  `If the plain route shows stale managed waterfall evidence, archived/completed shared slots, or targets that do not cover the selected sender set, immediately call resolve_campaign_fill_route({ intent: "active"${workspaceId ? `, workspaceId: "${workspaceId}"` : ""} }) and inspect current dashboard-active ACTIVE/PAUSED campaign-backed sequence campaigns before declaring a sender blocked.`,
219
303
  workspaceId
@@ -231,7 +315,7 @@ export function refillSendsCommand(input = {}) {
231
315
  "Fresh reread get_campaign_refill_state immediately before any import, prep, approval, or horizon-fill mutation.",
232
316
  "Maintain a target-window saturation ledger per selected sender from get_refill_target_plan: selected days, gross capacity, actual sent counts, future scheduler-owned scheduled counts, projected counts, ready-to-schedule buffer, remaining projected gap, paid InMail threshold feasibility, targetShapeRevision, stateRevision, and next MCP primitive.",
233
317
  "Source/fallback primitives require receipt-proven exhaustion: existingRowFrontier.hasMoreFrontierRows:false, approvalCandidates:0, no fresh active prep, no stuckActiveCells, and no non-terminal approvedNotDispatched rows. Treat anomalies, stuckActiveCells, and non-terminal approvedNotDispatched as diagnose-and-report gates, not exhaustion. Terminal approvedNotDispatched blockers may be reported and then the ladder can proceed.",
234
- "In manual/non-yolo, scheduled, and --yolo modes, this tool may automatically refresh stale or missing paid-InMail credit facts once per selected sender when the first target plan returns refresh_paid_inmail_credits candidates. This is the only newly allowed automatic write in manual/non-yolo mode; it reruns get_refill_target_plan and returns autoPaidInmailRefresh with attemptedSenderIds, refreshedPaidInmailSenderIds, failedPaidInmailRefreshes, and the post-refresh targetPlan before any prep/source-copy/bounded-approval/read-only wait action is chosen.",
318
+ "In manual/non-yolo, scheduled, and --yolo modes, this tool may automatically refresh stale or missing paid-InMail credit facts once per selected sender when the first target plan returns refresh_paid_inmail_credits candidates. This is the only newly allowed automatic write in manual/non-yolo mode; it reruns get_refill_target_plan and returns autoPaidInmailRefresh with attemptedSenderIds, refreshedPaidInmailSenderIds, failedPaidInmailRefreshes, and the post-refresh targetPlan before any prep/source-copy/bounded-approval/exact-date-sweep/read-only-wait action is chosen.",
235
319
  "Freshness gate precedes scheduler wait: if any selected target.senderRefillPlans[].paidInmail.status is missing_credit_facts or stale_credit_facts, or the target plan contains refresh_paid_inmail_credits, do not enter wait_for_scheduler even when remainingReadyOrProjectedGap is 0; refresh exact sender credit facts once, reread get_refill_target_plan, then choose scheduler wait only if freshness is clean.",
236
320
  "Scheduler-run receipt interpretation: cellsConsidered is allocation-attempt count, readyCellsFound is ready inventory before prefilters, and campaignScopeSummary must include the selected campaign/table before inferring it was ready-but-blocked. Read prefiltered, skipped, and deferred separately. For ready closed-InMail cells with stale paid-credit facts, route to refresh_paid_inmail_credits_then_rerun once, then rerun/status. wait_for_capacity_or_window means report loaded/capped/waiting and do not source or prep more rows. no_ready_cells_continue_refill_prep means return to the refill/prep ladder.",
237
321
  "For wait_for_active_work and wait_for_scheduler, honor the receipt's absolute wait.deadlineAt when present. If the deadline is expired on this call, escalate to diagnostics with the receipt evidence instead of issuing another blind wait.",
@@ -241,9 +325,9 @@ export function refillSendsCommand(input = {}) {
241
325
  "Do not complete a fill/schedule request until a final get_refill_target_plan or refill-state reread proves projected coverage (sent + scheduled) fills the scheduler-forward target window. Treat awaiting_scheduler_after_ready_buffer as loaded, awaiting scheduler when ready buffer covers the gap; do not import/prep more rows in that state, and keep polling unless Christian stops/statuses the run or a concrete non-scheduler blocker such as paid_inmail_below_threshold appears.",
242
326
  ],
243
327
  approvalContract: yolo
244
- ? "Auto-accept only the rendered bounded refill target packet after get_refill_target_plan and fresh reread. Execute one globally ranked primitive from target.globalActionQueue at a time, then rerun get_refill_target_plan before taking another action. Continue through every safe prep/source-copy/bounded-approval/read-only wait action inside that packet, but source-copy/fallback requires receipt-proven exhaustion of earlier ready/prep/approval rungs. Unbounded approval, start_campaign, source-family switches, threshold changes, and campaign creation require their own explicit gates. After each safe action, rerun get_refill_target_plan; keep going while targetShapeRevision is stable and projected coverage (sent + scheduled) progresses toward the target, even though stateRevision changes. Ready-to-schedule rows are buffer, not completion. If ready buffer covers the projected gap, use only persistent read-only scheduler wait/reread and keep the run open until projected coverage fills or Christian stops/statuses it. Stop immediately if targetShapeRevision changes because sender set, route, ids, caps, dates, blockers, action class, paid InMail threshold feasibility, side-effect class, or receipt exhaustion proof drifts. --yolo does not authorize lowering paid InMail thresholds or creating connection campaigns."
328
+ ? "Auto-accept only the rendered bounded refill target packet after get_refill_target_plan and fresh reread. Execute one globally ranked primitive from target.globalActionQueue at a time, then rerun get_refill_target_plan before taking another action. Continue through every safe prep/source-copy/bounded-approval/exact-date-sweep/read-only-wait action inside that packet, but source-copy/fallback requires receipt-proven exhaustion of earlier ready/prep/approval rungs. Unbounded approval, start_campaign, source-family switches, threshold changes, and campaign creation require their own explicit gates. After each safe action, rerun get_refill_target_plan; keep going while targetShapeRevision is stable and projected coverage (sent + scheduled) progresses toward the target, even though stateRevision changes. Ready-to-schedule rows are buffer, not completion. If ready buffer covers the projected gap and the exact-date run_scheduler_sweep is target.globalActionQueue[0], execute it once and fully reread. Only non-exact or no-sweep scheduler waits are read-only; use a persistent read-only scheduler wait/reread and keep the run open until projected coverage fills or Christian stops/statuses it. Stop immediately if targetShapeRevision changes because workspace, sender set, campaign/table/stable source identity, configured caps, exact date/window, lane, or the approved action/side-effect envelope drifts. Treat coverage, blockers, receipts, timestamps, and current action progress as stateRevision changes inside that envelope. --yolo does not authorize lowering paid InMail thresholds or creating connection campaigns."
245
329
  : hasSenderSelectors
246
- ? "Before mutation, post the full bounded refill packet in normal chat as Markdown, including workspace, sender scope, campaign table, exact ids, caps/dates, gross target, sent count, scheduled count, projected count, ready buffer, remaining projected gap, paid InMail threshold feasibility, targetShapeRevision/stateRevision, side effects, forbidden actions, and stop condition. Then ask the final Accept/Decline structured approval question with a compact body that refers back to the posted packet instead of duplicating it. Only ask when get_refill_target_plan reports a positive remaining projected/ready gap. If target is complete by projected coverage, no-op without approval. If ready buffer covers the projected gap, run only persistent read-only scheduler wait/reread and keep the run open until projected coverage fills or Christian stops/statuses it. Threshold changes and campaign creation need separate explicit approval."
330
+ ? "Before mutation, post the full bounded refill packet in normal chat as Markdown, including workspace, sender scope, campaign table, exact ids, caps/dates, gross target, sent count, scheduled count, projected count, ready buffer, remaining projected gap, paid InMail threshold feasibility, targetShapeRevision/stateRevision, side effects, forbidden actions, and stop condition. Then ask the final Accept/Decline structured approval question with a compact body that refers back to the posted packet instead of duplicating it. Only ask when get_refill_target_plan reports a positive remaining projected/ready gap. If target is complete by projected coverage, no-op without approval. If ready buffer covers the projected gap and the approved exact-date run_scheduler_sweep is target.globalActionQueue[0], execute it once and fully reread. Only non-exact or no-sweep scheduler waits are read-only; keep the run open until projected coverage fills or Christian stops/statuses it. Threshold changes and campaign creation need separate explicit approval."
247
331
  : "Ask which eligible enrolled senders to refill first, then, before mutation, post the full bounded refill packet in normal chat as Markdown and ask the final Accept/Decline structured approval question with a compact body that refers back to the posted packet.",
248
332
  forbiddenActions: [
249
333
  "start_campaign outside an exact selected PAUSED campaign-backed refill packet",
@@ -276,6 +360,7 @@ export function refillSendsCommand(input = {}) {
276
360
  senders,
277
361
  senderIds,
278
362
  senderNames,
363
+ ...(actionTypes ? { actionTypes } : {}),
279
364
  targetDate,
280
365
  untilDate,
281
366
  horizonSendDays,
@@ -284,6 +369,14 @@ export function refillSendsCommand(input = {}) {
284
369
  intent,
285
370
  approvalMode,
286
371
  workspaceId,
372
+ ...(input.expectedTargetShapeRevision
373
+ ? {
374
+ expectedTargetShapeRevision: input.expectedTargetShapeRevision,
375
+ }
376
+ : {}),
377
+ ...(input.expectedActionKey
378
+ ? { expectedActionKey: input.expectedActionKey }
379
+ : {}),
287
380
  },
288
381
  },
289
382
  },
@@ -307,6 +400,7 @@ function targetPlanInputFor(input = {}) {
307
400
  ...(senderIds.length > 0 ? { senderIds } : {}),
308
401
  ...(senderNames.length > 0 ? { senderNames } : {}),
309
402
  ...(senders.length > 0 ? { senders } : {}),
403
+ ...(input.actionTypes ? { actionTypes: input.actionTypes } : {}),
310
404
  ...(targetDate
311
405
  ? { targetDate }
312
406
  : untilDate
@@ -327,6 +421,24 @@ function targetPlanFingerprint(plan) {
327
421
  generatedAt: stringValue(root?.generatedAt) ?? null,
328
422
  };
329
423
  }
424
+ function approvedPacketComparison(input, targetPlan) {
425
+ const root = recordValue(targetPlan);
426
+ const action = recordValue(firstGlobalAction(targetPlan));
427
+ const actualTargetShapeRevision = stringValue(root?.targetShapeRevision) ?? null;
428
+ const actualActionKey = stringValue(action?.actionKey) ?? null;
429
+ const shapeChanged = Boolean(input.expectedTargetShapeRevision &&
430
+ input.expectedTargetShapeRevision !== actualTargetShapeRevision);
431
+ const actionChanged = Boolean(input.expectedActionKey && input.expectedActionKey !== actualActionKey);
432
+ return {
433
+ changed: shapeChanged || actionChanged,
434
+ expectedTargetShapeRevision: input.expectedTargetShapeRevision ?? null,
435
+ actualTargetShapeRevision,
436
+ expectedActionKey: input.expectedActionKey ?? null,
437
+ actualActionKey,
438
+ shapeChanged,
439
+ actionChanged,
440
+ };
441
+ }
330
442
  function paidRefreshApprovalPacket(params) {
331
443
  const action = params.action.action ?? {};
332
444
  const ids = actionIds(action);
@@ -357,13 +469,61 @@ function paidRefreshOptionsFromApprovalPacket(packet) {
357
469
  maxStalenessSeconds: packet.maxStalenessSeconds,
358
470
  };
359
471
  }
472
+ function durableV2PlanEligible(plan) {
473
+ const root = recordValue(plan);
474
+ const target = recordValue(root?.target);
475
+ const senderPlans = Array.isArray(target?.senderRefillPlans)
476
+ ? target.senderRefillPlans
477
+ : [];
478
+ return Boolean(stringValue(root?.targetShapeRevision) &&
479
+ senderPlans.some((entry) => {
480
+ const senderPlan = recordValue(entry);
481
+ return Array.isArray(senderPlan?.laneLedgers);
482
+ }));
483
+ }
484
+ function durableApprovalEcho(input) {
485
+ const values = {
486
+ scopeHash: stringValue(input.scopeHash),
487
+ targetShapeRevision: stringValue(input.targetShapeRevision),
488
+ actionFingerprint: stringValue(input.actionFingerprint),
489
+ expectedTargetsHash: stringValue(input.expectedTargetsHash),
490
+ approvalExpiresAt: stringValue(input.approvalExpiresAt),
491
+ approvalFingerprint: stringValue(input.approvalFingerprint),
492
+ };
493
+ return Object.values(values).every(Boolean)
494
+ ? values
495
+ : null;
496
+ }
497
+ function durableYoloBaseResult(params) {
498
+ return {
499
+ ...params.command,
500
+ targetPlan: params.targetPlan,
501
+ targetPlanBeforePaidRefresh: null,
502
+ targetPlanBeforeYoloPrimitive: params.targetPlan,
503
+ autoPaidInmailRefresh: {
504
+ enabled: false,
505
+ status: "skipped_awaiting_durable_approval",
506
+ refreshedPaidInmailSenderIds: [],
507
+ failedPaidInmailRefreshes: [],
508
+ refreshReceipts: [],
509
+ attemptedSenderIds: [],
510
+ targetPlanReread: false,
511
+ workspaceId: params.workspaceId,
512
+ workspaceResolution: "explicit",
513
+ note: "Durable v2 binds approval before credit refresh or any refill primitive.",
514
+ },
515
+ };
516
+ }
360
517
  export async function executeRefillSendsCommand(input = {}) {
361
- const yolo = input.yolo === true;
362
- const executionMode = input.executionMode ?? (yolo ? "yolo" : "manual");
363
- const mustHaveWorkspace = yolo || input.requireWorkspace === true || executionMode === "scheduled";
518
+ const normalizedInput = normalizeRefillSendsInput(input);
519
+ const yolo = normalizedInput.yolo === true;
520
+ const executionMode = normalizedInput.executionMode ?? (yolo ? "yolo" : "manual");
521
+ const mustHaveWorkspace = yolo ||
522
+ normalizedInput.requireWorkspace === true ||
523
+ executionMode === "scheduled";
364
524
  const workspaceContext = mustHaveWorkspace
365
525
  ? createWorkspaceContext({
366
- workspaceId: input.workspaceId,
526
+ workspaceId: normalizedInput.workspaceId,
367
527
  executionMode,
368
528
  toolName: "refill_sends",
369
529
  })
@@ -372,9 +532,12 @@ export async function executeRefillSendsCommand(input = {}) {
372
532
  return workspaceContext;
373
533
  }
374
534
  const scopedInput = workspaceContext && workspaceContext.ok
375
- ? { ...input, workspaceId: workspaceContext.context.workspaceId }
376
- : input;
377
- const command = refillSendsCommand(scopedInput);
535
+ ? {
536
+ ...normalizedInput,
537
+ workspaceId: workspaceContext.context.workspaceId,
538
+ }
539
+ : normalizedInput;
540
+ const command = buildRefillSendsCommand(scopedInput);
378
541
  const workspaceId = workspaceContext && workspaceContext.ok
379
542
  ? workspaceContext.context.workspaceId
380
543
  : normalizeExplicitWorkspaceId(scopedInput.workspaceId);
@@ -387,6 +550,7 @@ export async function executeRefillSendsCommand(input = {}) {
387
550
  status: "not_run_without_yolo",
388
551
  refreshedPaidInmailSenderIds: [],
389
552
  failedPaidInmailRefreshes: [],
553
+ refreshReceipts: [],
390
554
  note: "Automatic paid InMail credit refresh requires --yolo, scheduled mode, or an explicit workspaceId so the request can refresh exact sender facts safely.",
391
555
  },
392
556
  yoloExecution: {
@@ -399,6 +563,103 @@ export async function executeRefillSendsCommand(input = {}) {
399
563
  }
400
564
  const targetPlanInput = targetPlanInputFor(scopedInput);
401
565
  const targetPlanBeforePaidRefresh = await getRefillTargetPlan(targetPlanInput);
566
+ const approvedPacket = approvedPacketComparison(scopedInput, targetPlanBeforePaidRefresh);
567
+ if (approvedPacket.changed) {
568
+ return {
569
+ ...command,
570
+ ok: false,
571
+ code: "approval_scope_changed",
572
+ targetPlan: targetPlanBeforePaidRefresh,
573
+ targetPlanBeforePaidRefresh: null,
574
+ targetPlanBeforeYoloPrimitive: null,
575
+ ...approvedPacket,
576
+ autoPaidInmailRefresh: {
577
+ enabled: false,
578
+ status: "skipped_approval_scope_changed",
579
+ refreshedPaidInmailSenderIds: [],
580
+ failedPaidInmailRefreshes: [],
581
+ refreshReceipts: [],
582
+ attemptedSenderIds: [],
583
+ targetPlanReread: false,
584
+ workspaceId,
585
+ workspaceResolution: workspaceId ? "explicit" : "active_config",
586
+ note: "Approved refill scope changed before paid-credit refresh or primitive execution.",
587
+ },
588
+ yoloExecution: {
589
+ enabled: yolo,
590
+ status: "approval_scope_changed",
591
+ selectedAction: firstGlobalAction(targetPlanBeforePaidRefresh),
592
+ targetPlanReread: false,
593
+ },
594
+ };
595
+ }
596
+ if (yolo &&
597
+ workspaceId &&
598
+ durableV2PlanEligible(targetPlanBeforePaidRefresh)) {
599
+ const base = durableYoloBaseResult({
600
+ command,
601
+ targetPlan: targetPlanBeforePaidRefresh,
602
+ workspaceId,
603
+ });
604
+ const echo = durableApprovalEcho(scopedInput);
605
+ if (!scopedInput.approvalFingerprint) {
606
+ const approval = await prepareRefillSendsV2Approval({
607
+ workspaceId,
608
+ intent: scopedInput.intent,
609
+ senderIds: normalizeStrings(scopedInput.senderIds),
610
+ approvalMode: scopedInput.approvalMode ?? "approve",
611
+ targetDate: scopedInput.targetDate ?? undefined,
612
+ untilDate: scopedInput.untilDate ?? undefined,
613
+ horizonSendDays: scopedInput.horizonSendDays,
614
+ targetPlan: targetPlanBeforePaidRefresh,
615
+ });
616
+ return {
617
+ ...base,
618
+ yoloExecution: {
619
+ enabled: true,
620
+ ...approval,
621
+ selectedAction: firstGlobalAction(targetPlanBeforePaidRefresh),
622
+ targetPlanReread: false,
623
+ },
624
+ };
625
+ }
626
+ if (!echo || !scopedInput.runId || typeof scopedInput.fence !== "number") {
627
+ return {
628
+ ...base,
629
+ ok: false,
630
+ code: "approval_scope_changed",
631
+ yoloExecution: {
632
+ enabled: true,
633
+ status: "approval_scope_changed",
634
+ blocker: "approval_echo_incomplete",
635
+ selectedAction: firstGlobalAction(targetPlanBeforePaidRefresh),
636
+ targetPlanReread: false,
637
+ },
638
+ };
639
+ }
640
+ const durable = await refillSendsV2Command({
641
+ workspaceId,
642
+ runId: scopedInput.runId,
643
+ fence: scopedInput.fence,
644
+ intent: scopedInput.intent,
645
+ senderIds: normalizeStrings(scopedInput.senderIds),
646
+ approvalMode: scopedInput.approvalMode ?? "approve",
647
+ targetDate: scopedInput.targetDate ?? undefined,
648
+ untilDate: scopedInput.untilDate ?? undefined,
649
+ horizonSendDays: scopedInput.horizonSendDays,
650
+ targetPlan: targetPlanBeforePaidRefresh,
651
+ approvalEcho: echo,
652
+ });
653
+ return {
654
+ ...base,
655
+ yoloExecution: {
656
+ enabled: true,
657
+ ...durable,
658
+ selectedAction: firstGlobalAction(targetPlanBeforePaidRefresh),
659
+ targetPlanReread: true,
660
+ },
661
+ };
662
+ }
402
663
  const refreshActions = collectPaidInmailRefreshActions(targetPlanBeforePaidRefresh);
403
664
  const refreshedPaidInmailSenderIds = [];
404
665
  const failedPaidInmailRefreshes = [];