@sellable/mcp 0.1.793 → 0.1.794

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 (54) hide show
  1. package/dist/agent-tool-names.d.ts +2 -2
  2. package/dist/agent-tool-names.js +14 -0
  3. package/dist/scheduler-envelope.d.ts +190 -0
  4. package/dist/scheduler-envelope.js +412 -0
  5. package/dist/server.js +28 -2
  6. package/dist/tools/campaign-message-preparation.d.ts +25 -0
  7. package/dist/tools/campaign-message-preparation.js +37 -0
  8. package/dist/tools/campaign-processing.d.ts +74 -1
  9. package/dist/tools/campaign-processing.js +57 -1
  10. package/dist/tools/prompts.d.ts +15 -3
  11. package/dist/tools/prompts.js +25 -0
  12. package/dist/tools/refill-executors.d.ts +43 -70
  13. package/dist/tools/refill-executors.js +201 -273
  14. package/dist/tools/refill-sends.d.ts +3 -3
  15. package/dist/tools/refill-sends.js +1 -1
  16. package/dist/tools/refill-target-plan.js +1 -1
  17. package/dist/tools/refill-v3-advance-contract.d.ts +618 -0
  18. package/dist/tools/refill-v3-advance-contract.js +920 -0
  19. package/dist/tools/refill-v3-advance.d.ts +397 -0
  20. package/dist/tools/refill-v3-advance.js +75 -0
  21. package/dist/tools/refill-v3-attention-packet-contract.d.ts +608 -0
  22. package/dist/tools/refill-v3-attention-packet-contract.js +911 -0
  23. package/dist/tools/refill-v3-campaign-work.d.ts +324 -0
  24. package/dist/tools/refill-v3-campaign-work.js +1015 -0
  25. package/dist/tools/refill-v3-continue-contract.d.ts +433 -0
  26. package/dist/tools/refill-v3-continue-contract.js +112 -0
  27. package/dist/tools/refill-v3-continue.d.ts +501 -0
  28. package/dist/tools/refill-v3-continue.js +654 -0
  29. package/dist/tools/refill-v3-edit-selected-campaign-contract.d.ts +208 -0
  30. package/dist/tools/refill-v3-edit-selected-campaign-contract.js +351 -0
  31. package/dist/tools/refill-v3-edit-selected-campaign.d.ts +232 -0
  32. package/dist/tools/refill-v3-edit-selected-campaign.js +147 -0
  33. package/dist/tools/refill-v3-fill-ready-contract.d.ts +198 -0
  34. package/dist/tools/refill-v3-fill-ready-contract.js +324 -0
  35. package/dist/tools/refill-v3-fill-ready.d.ts +199 -0
  36. package/dist/tools/refill-v3-fill-ready.js +136 -0
  37. package/dist/tools/refill-v3-source-family-dispatch.d.ts +249 -0
  38. package/dist/tools/refill-v3-source-family-dispatch.js +514 -0
  39. package/dist/tools/refill-v3-waterfall-contract.d.ts +156 -0
  40. package/dist/tools/refill-v3-waterfall-contract.js +181 -0
  41. package/dist/tools/refill-v3-waterfall.d.ts +136 -0
  42. package/dist/tools/refill-v3-waterfall.js +161 -0
  43. package/dist/tools/refill-v3-world-state-contract.d.ts +666 -0
  44. package/dist/tools/refill-v3-world-state-contract.js +790 -0
  45. package/dist/tools/refill-v3-world-state.d.ts +592 -0
  46. package/dist/tools/refill-v3-world-state.js +137 -0
  47. package/dist/tools/registry.d.ts +1710 -3
  48. package/dist/tools/registry.js +19 -0
  49. package/package.json +1 -1
  50. package/skills/refill-sends/SKILL.md +89 -4
  51. package/skills/refill-sends-waterfall-order/SKILL.md +174 -0
  52. package/skills/refill-sends-work-campaign/SKILL.md +253 -0
  53. package/skills/refill-sends-workflow/SKILL.md +112 -5
  54. package/skills/refill-sends-workflow/core/flow.v3.json +209 -0
@@ -3,14 +3,14 @@
3
3
  * manifests. Keep this free of tool implementation imports so Next can bundle
4
4
  * it; registry parity is enforced in sellable-agent-tool-policy.test.ts.
5
5
  */
6
- export declare const SELLABLE_MCP_TOOL_NAMES: readonly ["add_column", "add_on_demand_leads", "add_rubric_item", "add_teammate", "add_to_comment_campaign", "add_to_connection_campaign", "add_to_inmail_campaign", "archive_campaign", "attach_recommended_sequence", "attach_sequence", "bootstrap_create_campaign", "bootstrap_engage", "bootstrap_engage_multi", "bootstrap_find_leads", "bulk_enrich_with_prospeo", "calculate_linkedin_hook_preview", "cancel_campaign_message_preparation", "cancel_find_leads", "cancel_lead_import", "capture_post_idea", "check_inbox_reply_eligibility", "check_rubric", "commit_blueprint", "complete_sender_research", "confirm_harvest_job_companies", "confirm_lead_list", "confirm_prospeo_company_accounts", "copy_sender_config", "create_campaign", "create_on_demand_campaign", "create_on_demand_table", "create_workflow_table", "create_workspace", "delete_column", "delete_rubric_item", "draft_rubrics", "duplicate_campaign", "enrich_with_prospeo", "export_table_csv", "export_workspace_csv", "fetch_company", "fetch_company_posts", "fetch_linkedin_posts", "fetch_linkedin_profile", "fetch_post_engagers", "fill_campaign_horizon", "get_active_workspace", "get_auth_status", "get_campaign", "get_campaign_context", "get_campaign_framework", "get_campaign_message_preparation_status", "get_campaign_messages_preview", "get_campaign_navigation_state", "get_campaign_refill_state", "get_campaign_table_schema", "get_campaign_waterfall", "get_campaigns", "get_column_schema", "get_company_info", "get_engage_memory", "get_engage_state", "get_engaged_posts", "get_find_leads_run", "get_inbox_thread", "get_integration_connection_status", "get_linkedin_profile", "get_or_create_direct_campaign_table", "get_post_draft", "get_post_find_leads_scout_registry", "get_post_idea", "get_prospeo_credits", "get_provider_prompt", "get_published_post", "get_refill_target_plan", "get_rows", "get_rows_minimal", "get_runtime_identity", "get_scheduler_fill_capacity", "get_sender", "get_sender_routing", "get_source_scout_registry", "get_subskill_asset", "get_subskill_prompt", "get_table_rows", "get_user_posts", "get_workspace", "import_leads", "init_on_demand_sequence", "integrations_call_tool", "integrations_describe_tool", "integrations_list_tools", "list_column_types", "list_dnc_entries", "list_post_draft_iterations", "list_post_drafts", "list_post_ideas", "list_published_posts", "list_senders", "list_subskill_prompts", "list_tables", "list_workspaces", "load_csv_dnc_entries", "load_csv_domains", "load_csv_linkedin_leads", "lookup_sales_nav_filter", "mark_post_published", "migrate_flat_configs", "pause_campaign", "pause_direct_campaign", "pause_on_demand_campaign", "preflight_find_leads_provider", "prepare_campaign_ab_test", "queue_campaign_cells", "queue_cells", "record_campaign_review_batch", "record_engage_proven_search", "refill_sends", "refresh_paid_inmail_credits", "refresh_sender_engagement", "reissue_find_leads_watch_link", "render_linkedin_post_preview", "reorder_columns", "resolve_campaign_fill_route", "revise_message_template_and_rerun", "run_scheduler_sweep", "save_domain_filters", "save_hook_research", "save_post_draft", "save_rubrics", "search_apollo", "search_available_integrations", "search_engagement_posts", "search_harvest_jobs", "search_inbox_threads", "search_prospeo", "search_prospeo_companies", "search_sales_nav", "search_signals", "search_subskill_prompts", "select_campaign_cells", "select_necessary_rubrics", "select_promising_posts", "send_inbox_draft", "send_inbox_manual_reply", "set_active_workspace", "set_campaign_waterfall_order", "set_company_info", "set_engage_state", "set_engage_style_guide", "set_headline_icp_criteria", "set_sender_calendar_link", "set_sender_routing", "setup_evergreen_campaigns", "start_campaign", "start_campaign_message_preparation", "start_cli_login", "start_direct_campaign", "start_integration_connection", "start_on_demand_campaign", "update_campaign", "update_campaign_brief", "update_cell", "update_column", "update_find_leads_run", "update_inbox_draft", "update_post_draft", "update_published_post_metrics", "update_rubric_item", "upsert_engage_tracked_person", "upsert_rubric", "validate_campaign1_kickoff_handoff", "verify_table_row", "wait_for_campaign_processing", "wait_for_campaign_table_ready", "wait_for_cli_login", "wait_for_find_leads_run", "wait_for_lead_list_ready", "wait_for_rubric_results"];
6
+ export declare const SELLABLE_MCP_TOOL_NAMES: readonly ["add_column", "add_on_demand_leads", "add_rubric_item", "add_teammate", "add_to_comment_campaign", "add_to_connection_campaign", "add_to_inmail_campaign", "archive_campaign", "attach_recommended_sequence", "attach_sequence", "bootstrap_create_campaign", "bootstrap_engage", "bootstrap_engage_multi", "bootstrap_find_leads", "bulk_enrich_with_prospeo", "calculate_linkedin_hook_preview", "cancel_campaign_message_preparation", "cancel_find_leads", "cancel_lead_import", "capture_post_idea", "check_inbox_reply_eligibility", "check_rubric", "commit_blueprint", "complete_sender_research", "confirm_harvest_job_companies", "confirm_lead_list", "confirm_prospeo_company_accounts", "copy_sender_config", "create_campaign", "create_on_demand_campaign", "create_on_demand_table", "create_workflow_table", "create_workspace", "delete_column", "delete_rubric_item", "draft_rubrics", "duplicate_campaign", "enrich_with_prospeo", "export_table_csv", "export_workspace_csv", "fetch_company", "fetch_company_posts", "fetch_linkedin_posts", "fetch_linkedin_profile", "fetch_post_engagers", "fill_campaign_horizon", "get_active_workspace", "get_auth_status", "get_campaign", "get_campaign_context", "get_campaign_framework", "get_campaign_message_preparation_status", "get_campaign_messages_preview", "get_campaign_navigation_state", "get_campaign_refill_state", "get_campaign_table_schema", "get_campaign_waterfall", "get_campaigns", "get_column_schema", "get_company_info", "get_engage_memory", "get_engage_state", "get_engaged_posts", "get_find_leads_run", "get_inbox_thread", "get_integration_connection_status", "get_linkedin_profile", "get_or_create_direct_campaign_table", "get_post_draft", "get_post_find_leads_scout_registry", "get_post_idea", "get_prospeo_credits", "get_provider_prompt", "get_published_post", "get_refill_target_plan", "get_rows", "get_rows_minimal", "get_runtime_identity", "get_scheduler_fill_capacity", "get_sender", "get_sender_routing", "get_source_scout_registry", "get_subskill_asset", "get_subskill_prompt", "get_table_rows", "get_user_posts", "get_workspace", "import_leads", "init_on_demand_sequence", "integrations_call_tool", "integrations_describe_tool", "integrations_list_tools", "list_column_types", "list_dnc_entries", "list_post_draft_iterations", "list_post_drafts", "list_post_ideas", "list_published_posts", "list_senders", "list_subskill_prompts", "list_tables", "list_workspaces", "load_csv_dnc_entries", "load_csv_domains", "load_csv_linkedin_leads", "lookup_sales_nav_filter", "mark_post_published", "migrate_flat_configs", "pause_campaign", "pause_direct_campaign", "pause_on_demand_campaign", "preflight_find_leads_provider", "prepare_campaign_ab_test", "queue_campaign_cells", "queue_cells", "record_campaign_review_batch", "record_engage_proven_search", "refill_sends", "refill_v3_advance", "refill_v3_continue", "refill_v3_edit_selected_campaign", "refill_v3_fill_ready", "refill_v3_world_state", "refresh_paid_inmail_credits", "refresh_sender_engagement", "reissue_find_leads_watch_link", "render_linkedin_post_preview", "reorder_columns", "resolve_campaign_fill_route", "revise_message_template_and_rerun", "run_scheduler_sweep", "save_domain_filters", "save_hook_research", "save_post_draft", "save_rubrics", "search_apollo", "search_available_integrations", "search_engagement_posts", "search_harvest_jobs", "search_inbox_threads", "search_prospeo", "search_prospeo_companies", "search_sales_nav", "search_signals", "search_subskill_prompts", "select_campaign_cells", "select_necessary_rubrics", "select_promising_posts", "send_inbox_draft", "send_inbox_manual_reply", "set_active_workspace", "set_campaign_waterfall_order", "set_company_info", "set_engage_state", "set_engage_style_guide", "set_headline_icp_criteria", "set_sender_calendar_link", "set_sender_routing", "setup_evergreen_campaigns", "start_campaign", "start_campaign_message_preparation", "start_cli_login", "start_direct_campaign", "start_integration_connection", "start_on_demand_campaign", "update_campaign", "update_campaign_brief", "update_cell", "update_column", "update_find_leads_run", "update_inbox_draft", "update_post_draft", "update_published_post_metrics", "update_rubric_item", "upsert_engage_tracked_person", "upsert_rubric", "validate_campaign1_kickoff_handoff", "verify_table_row", "wait_for_campaign_processing", "wait_for_campaign_table_ready", "wait_for_cli_login", "wait_for_find_leads_run", "wait_for_lead_list_ready", "wait_for_rubric_results"];
7
7
  /**
8
8
  * Explicit mutation classification lives beside (but outside) the flat tool
9
9
  * registry. Neither this list nor `isMutatingTool` is an input to
10
10
  * `compileAgentToolPolicy`, so editing it cannot move the fleet-pinned
11
11
  * `policyHash`.
12
12
  */
13
- export declare const MUTATING_TOOL_NAMES: readonly ["pause_campaign", "archive_campaign", "update_campaign", "fill_campaign_horizon", "set_campaign_waterfall_order", "set_sender_routing", "send_inbox_draft", "send_inbox_manual_reply", "load_csv_dnc_entries", "update_cell", "queue_cells"];
13
+ export declare const MUTATING_TOOL_NAMES: readonly ["pause_campaign", "archive_campaign", "update_campaign", "fill_campaign_horizon", "set_campaign_waterfall_order", "set_sender_routing", "send_inbox_draft", "send_inbox_manual_reply", "load_csv_dnc_entries", "update_cell", "queue_cells", "refill_v3_fill_ready", "refill_v3_edit_selected_campaign", "refill_v3_advance", "refill_v3_continue"];
14
14
  export declare function isMutatingTool(name: string): boolean;
15
15
  export declare const CROSS_WORKSPACE_EXPLICIT_WORKSPACE_REQUIRED: {
16
16
  readonly code: "cross_workspace_explicit_workspace_required";
@@ -122,6 +122,11 @@ export const SELLABLE_MCP_TOOL_NAMES = [
122
122
  "record_campaign_review_batch",
123
123
  "record_engage_proven_search",
124
124
  "refill_sends",
125
+ "refill_v3_advance",
126
+ "refill_v3_continue",
127
+ "refill_v3_edit_selected_campaign",
128
+ "refill_v3_fill_ready",
129
+ "refill_v3_world_state",
125
130
  "refresh_paid_inmail_credits",
126
131
  "refresh_sender_engagement",
127
132
  "reissue_find_leads_watch_link",
@@ -202,6 +207,15 @@ export const MUTATING_TOOL_NAMES = [
202
207
  "load_csv_dnc_entries",
203
208
  "update_cell",
204
209
  "queue_cells",
210
+ // Phase 146.5 Plan 08: the bounded Refill V3 mutation. Its sibling
211
+ // `refill_v3_world_state` is an OBSERVATION and deliberately stays out.
212
+ "refill_v3_fill_ready",
213
+ // Phase 146.5 Plan 09: the narrow selected-campaign config/filter/source edit.
214
+ "refill_v3_edit_selected_campaign",
215
+ // Phase 146.5 Plan 10: the one-step coordinator. It executes at most ONE
216
+ // existing mutation authority per invocation, so it carries the guard.
217
+ "refill_v3_advance",
218
+ "refill_v3_continue",
205
219
  ];
206
220
  const MUTATING_TOOL_NAME_SET = new Set(MUTATING_TOOL_NAMES);
207
221
  export function isMutatingTool(name) {
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Scheduler envelope PACKAGE seam.
3
+ *
4
+ * This module is the SOLE owner of the scheduler envelope/receipt
5
+ * interpretation:
6
+ *
7
+ * - `schedulerEnvelopeIsTerminal` — did THIS dispatch finish;
8
+ * - `normalizeSchedulerPrimitiveResult` — the one primitive-result normalizer;
9
+ * - `classifySchedulerReceiptState` — the closed STRUCTURAL receipt state
10
+ * `executed | attached | superseded | typed_failure`; and
11
+ * - the primitive value readers and `normalizeSchedulerChangedCounts` those
12
+ * three depend on.
13
+ *
14
+ * It is deliberately DEPENDENCY-FREE. `mcp/sellable/src/tools/refill-executors.ts`
15
+ * imports and re-exports these symbols so its existing callers are unchanged,
16
+ * and the app service `src/lib/refill-v3/fill-ready.ts` imports ONLY this file.
17
+ * Importing the normalizers from `refill-executors.ts` directly would pull the
18
+ * whole MCP executor graph (`../api.js` `getApi`/`SellableApiError`, `./leads.js`,
19
+ * `./campaigns.js`, `./scheduler-run.js`) into the Next server bundle; the one
20
+ * established `src/lib` -> `mcp/sellable/src` seam is
21
+ * `src/lib/sellable-agent/security/agent-tool-policy.ts`, which re-exports a
22
+ * SELF-CONTAINED module. This file keeps that property.
23
+ *
24
+ * There is exactly ONE terminality predicate and ONE envelope normalizer in the
25
+ * codebase. A second interpretation inside a consumer is a defect by
26
+ * construction (error law 2: one rule = one owner = one enforcement point).
27
+ */
28
+ export declare function recordValue(value: unknown): Record<string, unknown> | null;
29
+ export declare function stringValue(value: unknown): string | null;
30
+ export declare function numberValue(value: unknown): number | null;
31
+ export declare function stringArray(value: unknown): string[];
32
+ /**
33
+ * The structural shape one expected scheduler target carries. Declared here so
34
+ * the dependency-free module never has to reach into the executor graph for a
35
+ * type; `SchedulerExpectedTargetInput` is structurally assignable to it.
36
+ */
37
+ export type SchedulerEnvelopeExpectedTarget = {
38
+ campaignId: string;
39
+ tableId: string;
40
+ refillLaneKey: string;
41
+ branchActionTypes: Array<"send_invite" | "send_inmail_open" | "send_inmail_closed">;
42
+ senderIds: string[];
43
+ };
44
+ export type SchedulerChangedCount = {
45
+ campaignId: string;
46
+ tableId: string;
47
+ actionType: string;
48
+ count: number;
49
+ };
50
+ export type SchedulerChangedCounts = {
51
+ complete: true;
52
+ truncated: false;
53
+ total: number;
54
+ byCampaignTableActionType: SchedulerChangedCount[];
55
+ byCampaignTableActionTypeSender: Array<SchedulerChangedCount & {
56
+ senderId: string;
57
+ }>;
58
+ };
59
+ export declare function normalizeSchedulerChangedCounts(value: unknown): SchedulerChangedCounts | null;
60
+ /**
61
+ * fix(112ao-2): the scheduler's ATTACHING envelope statuses. These describe a
62
+ * dispatch that joined an existing run rather than producing its own outcome.
63
+ * They are terminal ONLY when the attach actually yielded a terminal receipt.
64
+ */
65
+ export declare const SCHEDULER_ATTACHING_ENVELOPE_STATUSES: ReadonlySet<string>;
66
+ /** The envelope statuses that report an outcome of this dispatch directly. */
67
+ export declare const SCHEDULER_TERMINAL_ENVELOPE_STATUSES: ReadonlySet<string>;
68
+ /**
69
+ * The envelope statuses that DECLINE to dispatch and name a retry instant. The
70
+ * authority attempted nothing, so they are neither an outcome nor a failure.
71
+ */
72
+ export declare const SCHEDULER_DEFERRED_ENVELOPE_STATUSES: ReadonlySet<string>;
73
+ /**
74
+ * The only receipt statuses that constitute a settled outcome. A receipt
75
+ * without one of these is not evidence that anything finished.
76
+ */
77
+ export declare const SCHEDULER_TERMINAL_RECEIPT_STATUSES: ReadonlySet<string>;
78
+ /**
79
+ * A dispatch replaced BEFORE execution names itself with one of these fields.
80
+ * The state is decided from the marker's PRESENCE as a value, never from the
81
+ * absence of a receipt.
82
+ */
83
+ export declare const SCHEDULER_SUPERSEDED_MARKER_FIELDS: readonly ["supersededBy", "supersededAt"];
84
+ export declare function hasTerminalSchedulerReceipt(receipt: Record<string, unknown> | null): boolean;
85
+ export declare function schedulerEnvelopeIsTerminal(schedulerStatus: string, receipt?: Record<string, unknown> | null): boolean;
86
+ export declare const SCHEDULER_RECEIPT_STATES: readonly ["executed", "attached", "superseded", "deferred", "typed_failure"];
87
+ export type SchedulerReceiptState = (typeof SCHEDULER_RECEIPT_STATES)[number];
88
+ /**
89
+ * The one receipt status that reports a settled FAILURE. Consumers compare
90
+ * against this constant instead of re-declaring the status vocabulary.
91
+ */
92
+ export declare const SCHEDULER_FAILED_RECEIPT_STATUS: "failed";
93
+ export type SchedulerReceiptClassification = {
94
+ state: "executed";
95
+ terminal: true;
96
+ envelopeStatus: string;
97
+ receiptStatus: string;
98
+ receipt: Record<string, unknown>;
99
+ } | {
100
+ state: "attached";
101
+ /** True only when the joined run already yielded a terminal receipt. */
102
+ terminal: boolean;
103
+ envelopeStatus: string;
104
+ receiptStatus: string | null;
105
+ receipt: Record<string, unknown> | null;
106
+ } | {
107
+ /** Replaced before execution: NO completion, NO deferral, NO counts. */
108
+ state: "superseded";
109
+ terminal: false;
110
+ envelopeStatus: string;
111
+ supersededBy: string | null;
112
+ } | {
113
+ /**
114
+ * The authority DECLINED to dispatch and named when to come back: a
115
+ * workspace backoff window or another scope holding the lease. No work
116
+ * was attempted, so this is a WAIT — reporting it as a settled failure is
117
+ * what made a second placement in the same minute look broken.
118
+ */
119
+ state: "deferred";
120
+ terminal: false;
121
+ envelopeStatus: string;
122
+ code: string;
123
+ message: string;
124
+ retryAfterMs: number | null;
125
+ } | {
126
+ state: "typed_failure";
127
+ terminal: true;
128
+ envelopeStatus: string;
129
+ code: string;
130
+ message: string;
131
+ };
132
+ /**
133
+ * Discriminate one scheduler envelope into the closed receipt-state vocabulary
134
+ * STRUCTURALLY.
135
+ *
136
+ * Every branch is decided from a VALUE that is present — an explicit superseded
137
+ * marker, an error code, the envelope status's membership in a closed set, or
138
+ * the receipt's OWN status. No branch is decided from the bare absence of a
139
+ * receipt: a bare negated-receipt guard silently merges "no receipt" with
140
+ * "replayed receipt",
141
+ * which is exactly P0-1(A)/(B), the defect that froze Dotwork at 0/50 across 21
142
+ * runs.
143
+ */
144
+ export declare function classifySchedulerReceiptState(value: unknown): SchedulerReceiptClassification;
145
+ export declare function normalizeSchedulerPrimitiveResult(value: unknown, expected?: {
146
+ expectedTargets: SchedulerEnvelopeExpectedTarget[];
147
+ expectedTargetsHash: string;
148
+ maxPlacements: number;
149
+ }): {
150
+ retryAfterMs: number;
151
+ changedCounts: null;
152
+ auditComplete: false;
153
+ unsettledReceipt?: Record<string, unknown> | undefined;
154
+ status: "matching_scheduler_active";
155
+ schedulerStatus: string;
156
+ terminal: false;
157
+ disposition: "bounded_reread_wait";
158
+ receipt: null;
159
+ blocker?: undefined;
160
+ } | {
161
+ status: string;
162
+ schedulerStatus: string;
163
+ disposition: "bounded_backoff_wait" | "diagnostic_stop";
164
+ receipt: {} | null;
165
+ retryAfterMs: number | null;
166
+ changedCounts: null;
167
+ auditComplete: false;
168
+ blocker: string;
169
+ terminal?: undefined;
170
+ } | {
171
+ status: "scheduler_receipt_incomplete";
172
+ schedulerStatus: string;
173
+ receipt: {} | null;
174
+ retryAfterMs: number | null;
175
+ changedCounts: null;
176
+ auditComplete: false;
177
+ blocker: "scheduler_receipt_incomplete";
178
+ disposition?: undefined;
179
+ terminal?: undefined;
180
+ } | {
181
+ status: string;
182
+ terminal: boolean;
183
+ receipt: {} | null;
184
+ retryAfterMs: number | null;
185
+ changedCounts: SchedulerChangedCounts;
186
+ auditComplete: true;
187
+ schedulerStatus?: undefined;
188
+ disposition?: undefined;
189
+ blocker?: undefined;
190
+ };
@@ -0,0 +1,412 @@
1
+ /**
2
+ * Scheduler envelope PACKAGE seam.
3
+ *
4
+ * This module is the SOLE owner of the scheduler envelope/receipt
5
+ * interpretation:
6
+ *
7
+ * - `schedulerEnvelopeIsTerminal` — did THIS dispatch finish;
8
+ * - `normalizeSchedulerPrimitiveResult` — the one primitive-result normalizer;
9
+ * - `classifySchedulerReceiptState` — the closed STRUCTURAL receipt state
10
+ * `executed | attached | superseded | typed_failure`; and
11
+ * - the primitive value readers and `normalizeSchedulerChangedCounts` those
12
+ * three depend on.
13
+ *
14
+ * It is deliberately DEPENDENCY-FREE. `mcp/sellable/src/tools/refill-executors.ts`
15
+ * imports and re-exports these symbols so its existing callers are unchanged,
16
+ * and the app service `src/lib/refill-v3/fill-ready.ts` imports ONLY this file.
17
+ * Importing the normalizers from `refill-executors.ts` directly would pull the
18
+ * whole MCP executor graph (`../api.js` `getApi`/`SellableApiError`, `./leads.js`,
19
+ * `./campaigns.js`, `./scheduler-run.js`) into the Next server bundle; the one
20
+ * established `src/lib` -> `mcp/sellable/src` seam is
21
+ * `src/lib/sellable-agent/security/agent-tool-policy.ts`, which re-exports a
22
+ * SELF-CONTAINED module. This file keeps that property.
23
+ *
24
+ * There is exactly ONE terminality predicate and ONE envelope normalizer in the
25
+ * codebase. A second interpretation inside a consumer is a defect by
26
+ * construction (error law 2: one rule = one owner = one enforcement point).
27
+ */
28
+ // ───────────────────────────────────────────────────────────────────────────
29
+ // Primitive value readers
30
+ // ───────────────────────────────────────────────────────────────────────────
31
+ export function recordValue(value) {
32
+ return value && typeof value === "object" && !Array.isArray(value)
33
+ ? value
34
+ : null;
35
+ }
36
+ export function stringValue(value) {
37
+ return typeof value === "string" && value.trim() ? value.trim() : null;
38
+ }
39
+ export function numberValue(value) {
40
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
41
+ }
42
+ export function stringArray(value) {
43
+ if (!Array.isArray(value))
44
+ return [];
45
+ return value.filter((item) => typeof item === "string");
46
+ }
47
+ export function normalizeSchedulerChangedCounts(value) {
48
+ const changedCounts = recordValue(value);
49
+ if (!changedCounts ||
50
+ changedCounts.complete !== true ||
51
+ changedCounts.truncated !== false) {
52
+ return null;
53
+ }
54
+ const total = numberValue(changedCounts.total);
55
+ if (total === null || total < 0 || !Number.isInteger(total))
56
+ return null;
57
+ if (!Array.isArray(changedCounts.byCampaignTableActionType))
58
+ return null;
59
+ const groups = [];
60
+ const seen = new Set();
61
+ for (const rawGroup of changedCounts.byCampaignTableActionType) {
62
+ const group = recordValue(rawGroup);
63
+ const campaignId = stringValue(group?.campaignId);
64
+ const tableId = stringValue(group?.tableId);
65
+ const actionType = stringValue(group?.actionType);
66
+ const count = numberValue(group?.count);
67
+ if (!campaignId ||
68
+ !tableId ||
69
+ !actionType ||
70
+ count === null ||
71
+ count <= 0 ||
72
+ !Number.isInteger(count)) {
73
+ return null;
74
+ }
75
+ const key = `${campaignId}:${tableId}:${actionType}`;
76
+ if (seen.has(key))
77
+ return null;
78
+ seen.add(key);
79
+ groups.push({ campaignId, tableId, actionType, count });
80
+ }
81
+ groups.sort((a, b) => {
82
+ if (a.campaignId !== b.campaignId) {
83
+ return a.campaignId.localeCompare(b.campaignId);
84
+ }
85
+ if (a.tableId !== b.tableId)
86
+ return a.tableId.localeCompare(b.tableId);
87
+ return a.actionType.localeCompare(b.actionType);
88
+ });
89
+ if (groups.reduce((sum, group) => sum + group.count, 0) !== total) {
90
+ return null;
91
+ }
92
+ if (!Array.isArray(changedCounts.byCampaignTableActionTypeSender)) {
93
+ return null;
94
+ }
95
+ const senderGroups = [];
96
+ const senderSeen = new Set();
97
+ for (const rawGroup of changedCounts.byCampaignTableActionTypeSender) {
98
+ const group = recordValue(rawGroup);
99
+ const campaignId = stringValue(group?.campaignId);
100
+ const tableId = stringValue(group?.tableId);
101
+ const actionType = stringValue(group?.actionType);
102
+ const senderId = stringValue(group?.senderId);
103
+ const count = numberValue(group?.count);
104
+ if (!campaignId ||
105
+ !tableId ||
106
+ !actionType ||
107
+ !senderId ||
108
+ count === null ||
109
+ count <= 0 ||
110
+ !Number.isInteger(count)) {
111
+ return null;
112
+ }
113
+ const key = `${campaignId}:${tableId}:${actionType}:${senderId}`;
114
+ if (senderSeen.has(key))
115
+ return null;
116
+ senderSeen.add(key);
117
+ senderGroups.push({ campaignId, tableId, actionType, senderId, count });
118
+ }
119
+ if (senderGroups.reduce((sum, group) => sum + group.count, 0) !== total) {
120
+ return null;
121
+ }
122
+ return {
123
+ complete: true,
124
+ truncated: false,
125
+ total,
126
+ byCampaignTableActionType: groups,
127
+ byCampaignTableActionTypeSender: senderGroups,
128
+ };
129
+ }
130
+ function completeExpectedTargetSummary(value, expectedTargets, maxPlacements) {
131
+ const summary = recordValue(value);
132
+ const items = Array.isArray(summary?.items) ? summary.items : [];
133
+ if (summary?.complete !== true ||
134
+ summary.truncated !== false ||
135
+ numberValue(summary.requested) !== expectedTargets.length ||
136
+ numberValue(summary.included) == null ||
137
+ numberValue(summary.omitted) == null ||
138
+ numberValue(summary.included) + numberValue(summary.omitted) !==
139
+ expectedTargets.length ||
140
+ numberValue(summary.maxPlacements) !== maxPlacements ||
141
+ items.length !== expectedTargets.length) {
142
+ return false;
143
+ }
144
+ return items.every((rawItem, index) => {
145
+ const item = recordValue(rawItem);
146
+ const target = expectedTargets[index];
147
+ const senderIds = stringArray(item?.senderIds).sort();
148
+ const bySender = Array.isArray(item?.bySender) ? item.bySender : [];
149
+ const accounted = bySender
150
+ .map((entry) => stringValue(recordValue(entry)?.senderId))
151
+ .filter((senderId) => Boolean(senderId))
152
+ .sort();
153
+ return (Boolean(target) &&
154
+ item?.campaignId === target?.campaignId &&
155
+ item?.tableId === target?.tableId &&
156
+ item?.refillLaneKey === target?.refillLaneKey &&
157
+ (item?.outcome === "included" || item?.outcome === "omitted") &&
158
+ JSON.stringify(senderIds) === JSON.stringify(target?.senderIds) &&
159
+ JSON.stringify(accounted) === JSON.stringify(target?.senderIds));
160
+ });
161
+ }
162
+ // ───────────────────────────────────────────────────────────────────────────
163
+ // Envelope status vocabularies
164
+ // ───────────────────────────────────────────────────────────────────────────
165
+ /**
166
+ * fix(112ao-2): the scheduler's ATTACHING envelope statuses. These describe a
167
+ * dispatch that joined an existing run rather than producing its own outcome.
168
+ * They are terminal ONLY when the attach actually yielded a terminal receipt.
169
+ */
170
+ export const SCHEDULER_ATTACHING_ENVELOPE_STATUSES = new Set(["accepted", "attached"]);
171
+ /** The envelope statuses that report an outcome of this dispatch directly. */
172
+ export const SCHEDULER_TERMINAL_ENVELOPE_STATUSES = new Set(["ran", "window_closed_noop"]);
173
+ /**
174
+ * The envelope statuses that DECLINE to dispatch and name a retry instant. The
175
+ * authority attempted nothing, so they are neither an outcome nor a failure.
176
+ */
177
+ export const SCHEDULER_DEFERRED_ENVELOPE_STATUSES = new Set(["backoff"]);
178
+ /**
179
+ * The only receipt statuses that constitute a settled outcome. A receipt
180
+ * without one of these is not evidence that anything finished.
181
+ */
182
+ export const SCHEDULER_TERMINAL_RECEIPT_STATUSES = new Set(["ran", "window_closed_noop", "failed"]);
183
+ /**
184
+ * A dispatch replaced BEFORE execution names itself with one of these fields.
185
+ * The state is decided from the marker's PRESENCE as a value, never from the
186
+ * absence of a receipt.
187
+ */
188
+ export const SCHEDULER_SUPERSEDED_MARKER_FIELDS = [
189
+ "supersededBy",
190
+ "supersededAt",
191
+ ];
192
+ export function hasTerminalSchedulerReceipt(receipt) {
193
+ const status = receipt ? stringValue(receipt.status) : null;
194
+ return Boolean(status && SCHEDULER_TERMINAL_RECEIPT_STATUSES.has(status));
195
+ }
196
+ export function schedulerEnvelopeIsTerminal(schedulerStatus, receipt = null) {
197
+ if (SCHEDULER_TERMINAL_ENVELOPE_STATUSES.has(schedulerStatus))
198
+ return true;
199
+ return (SCHEDULER_ATTACHING_ENVELOPE_STATUSES.has(schedulerStatus) &&
200
+ hasTerminalSchedulerReceipt(receipt));
201
+ }
202
+ // ───────────────────────────────────────────────────────────────────────────
203
+ // Closed structural receipt state
204
+ // ───────────────────────────────────────────────────────────────────────────
205
+ export const SCHEDULER_RECEIPT_STATES = [
206
+ "executed",
207
+ "attached",
208
+ "superseded",
209
+ "deferred",
210
+ "typed_failure",
211
+ ];
212
+ /**
213
+ * The one receipt status that reports a settled FAILURE. Consumers compare
214
+ * against this constant instead of re-declaring the status vocabulary.
215
+ */
216
+ export const SCHEDULER_FAILED_RECEIPT_STATUS = "failed";
217
+ /**
218
+ * Discriminate one scheduler envelope into the closed receipt-state vocabulary
219
+ * STRUCTURALLY.
220
+ *
221
+ * Every branch is decided from a VALUE that is present — an explicit superseded
222
+ * marker, an error code, the envelope status's membership in a closed set, or
223
+ * the receipt's OWN status. No branch is decided from the bare absence of a
224
+ * receipt: a bare negated-receipt guard silently merges "no receipt" with
225
+ * "replayed receipt",
226
+ * which is exactly P0-1(A)/(B), the defect that froze Dotwork at 0/50 across 21
227
+ * runs.
228
+ */
229
+ export function classifySchedulerReceiptState(value) {
230
+ const response = recordValue(value);
231
+ const receipt = recordValue(response?.receipt);
232
+ const envelopeStatus = stringValue(response?.status) ?? stringValue(receipt?.status) ?? "unknown";
233
+ const errorCode = stringValue(recordValue(response?.error)?.code);
234
+ // 1. A transition REPLACED before execution names itself. It is checked first
235
+ // and records no completion, no deferral, and no counts.
236
+ for (const field of SCHEDULER_SUPERSEDED_MARKER_FIELDS) {
237
+ const marker = stringValue(receipt?.[field]) ?? stringValue(response?.[field]);
238
+ if (marker) {
239
+ return {
240
+ state: "superseded",
241
+ terminal: false,
242
+ envelopeStatus,
243
+ supersededBy: marker,
244
+ };
245
+ }
246
+ }
247
+ // 2. A typed failure carries either the uncertainty vocabulary or an explicit
248
+ // error code. Uncertainty stays its own code rather than collapsing into a
249
+ // generic failure.
250
+ if (envelopeStatus === "uncertain_outcome" ||
251
+ errorCode === "uncertain_outcome") {
252
+ return {
253
+ state: "typed_failure",
254
+ terminal: true,
255
+ envelopeStatus,
256
+ code: "uncertain_outcome",
257
+ message: "The matching scheduler attempt produced no terminal receipt evidence.",
258
+ };
259
+ }
260
+ // 2b. A DEFERRAL is named before any failure branch: the authority declined
261
+ // to dispatch and said when to return. It carries no receipt, no counts,
262
+ // and no settled outcome.
263
+ // A deferral that hands back a TERMINAL receipt is not a deferral at all:
264
+ // it is the completed work being replayed, and it keeps its facts.
265
+ if (SCHEDULER_DEFERRED_ENVELOPE_STATUSES.has(envelopeStatus) &&
266
+ !hasTerminalSchedulerReceipt(receipt)) {
267
+ return {
268
+ state: "deferred",
269
+ terminal: false,
270
+ envelopeStatus,
271
+ code: errorCode ?? envelopeStatus,
272
+ message: stringValue(recordValue(response?.error)?.message) ?? "",
273
+ retryAfterMs: numberValue(response?.retryAfterMs),
274
+ };
275
+ }
276
+ if (envelopeStatus === "failed" || errorCode) {
277
+ return {
278
+ state: "typed_failure",
279
+ terminal: true,
280
+ envelopeStatus,
281
+ code: errorCode ?? envelopeStatus,
282
+ message: stringValue(recordValue(response?.error)?.message) ?? "",
283
+ };
284
+ }
285
+ const receiptStatus = stringValue(receipt?.status);
286
+ const receiptIsTerminal = Boolean(receiptStatus && SCHEDULER_TERMINAL_RECEIPT_STATUSES.has(receiptStatus));
287
+ // 3. The envelope reports THIS dispatch's own outcome. Its receipt must carry
288
+ // a settled status; an incomplete one is a typed failure, never a success.
289
+ if (SCHEDULER_TERMINAL_ENVELOPE_STATUSES.has(envelopeStatus)) {
290
+ if (receipt && receiptIsTerminal && receiptStatus) {
291
+ return {
292
+ state: "executed",
293
+ terminal: true,
294
+ envelopeStatus,
295
+ receiptStatus,
296
+ receipt,
297
+ };
298
+ }
299
+ return {
300
+ state: "typed_failure",
301
+ terminal: true,
302
+ envelopeStatus,
303
+ code: "scheduler_receipt_incomplete",
304
+ message: "The terminal envelope carried no settled receipt status of its own.",
305
+ };
306
+ }
307
+ // 4. The dispatch joined an existing run. It completes only from a receipt
308
+ // that carries its own terminal status; a replayed terminal receipt is a
309
+ // legitimate idempotent completion and keeps its facts.
310
+ return {
311
+ state: "attached",
312
+ terminal: receiptIsTerminal,
313
+ envelopeStatus,
314
+ receiptStatus: receiptIsTerminal ? receiptStatus : null,
315
+ receipt: receiptIsTerminal ? receipt : null,
316
+ };
317
+ }
318
+ // ───────────────────────────────────────────────────────────────────────────
319
+ // Primitive result normalizer
320
+ // ───────────────────────────────────────────────────────────────────────────
321
+ export function normalizeSchedulerPrimitiveResult(value, expected) {
322
+ const response = recordValue(value);
323
+ const receipt = recordValue(response?.receipt);
324
+ const schedulerStatus = stringValue(response?.status) ?? stringValue(receipt?.status) ?? "unknown";
325
+ const errorCode = stringValue(recordValue(response?.error)?.code);
326
+ // fix(112ao-2): whether this dispatch finished is decided explicitly from the
327
+ // envelope status plus the RECEIPT'S OWN STATUS — never from the bare
328
+ // presence of a receipt field.
329
+ //
330
+ // The previous guard negated the receipt and OR-ed the attaching statuses.
331
+ // Truthiness is
332
+ // the wrong test: an attach that carries a receipt with no terminal status is
333
+ // not evidence that anything settled, yet it skipped this branch, ran the v3
334
+ // audit on that non-evidence, and could return auditComplete: true — a
335
+ // completion claim for a run still in flight, which the coordinator then
336
+ // treats as done rather than awaiting (Dotwork 2026-07-29: 21 exact runs,
337
+ // coverage frozen at sent:0/scheduled:0).
338
+ //
339
+ // The attach is NOT always in flight, and this deliberately does not treat it
340
+ // so: scheduler-run.ts only returns a receipt from
341
+ // waitForSchedulerRunReceipt when it is scope-matched to THIS request, so an
342
+ // attach carrying a terminal receipt is a legitimate idempotent completion of
343
+ // this exact operation and must keep its counters. Discarding it would report
344
+ // zero placements for a sweep that really placed cells.
345
+ //
346
+ // `ran`/`window_closed_noop` remain terminal. `backoff`/`failed` keep their
347
+ // existing handling — the run loop already unwraps a backoff envelope's
348
+ // replayed receipt deliberately (schedulerRunReceiptIsFreshZeroScheduled).
349
+ if (SCHEDULER_ATTACHING_ENVELOPE_STATUSES.has(schedulerStatus) &&
350
+ !hasTerminalSchedulerReceipt(receipt)) {
351
+ return {
352
+ status: "matching_scheduler_active",
353
+ schedulerStatus,
354
+ terminal: false,
355
+ disposition: "bounded_reread_wait",
356
+ receipt: null,
357
+ // A non-terminal receipt is not this dispatch's effect. Surfaced under a
358
+ // distinct name so it stays readable for diagnostics but can never be
359
+ // mistaken for a settled outcome.
360
+ ...(receipt ? { unsettledReceipt: receipt } : {}),
361
+ retryAfterMs: numberValue(response?.retryAfterMs) ?? 30_000,
362
+ changedCounts: null,
363
+ auditComplete: false,
364
+ };
365
+ }
366
+ if (errorCode === "scheduler_scope_busy" ||
367
+ errorCode === "uncertain_outcome" ||
368
+ schedulerStatus === "uncertain_outcome") {
369
+ const status = errorCode === "scheduler_scope_busy"
370
+ ? "scheduler_scope_busy"
371
+ : "uncertain_outcome";
372
+ return {
373
+ status,
374
+ schedulerStatus,
375
+ disposition: status === "scheduler_scope_busy"
376
+ ? "bounded_backoff_wait"
377
+ : "diagnostic_stop",
378
+ receipt: response?.receipt ?? null,
379
+ retryAfterMs: numberValue(response?.retryAfterMs),
380
+ changedCounts: null,
381
+ auditComplete: false,
382
+ blocker: status,
383
+ };
384
+ }
385
+ const changedCounts = normalizeSchedulerChangedCounts(receipt?.changedCounts ?? response?.changedCounts);
386
+ const targetAuditComplete = !expected ||
387
+ (receipt?.receiptVersion === 3 &&
388
+ receipt.expectedTargetsHash === expected.expectedTargetsHash &&
389
+ receipt.maxPlacements === expected.maxPlacements &&
390
+ completeExpectedTargetSummary(receipt.expectedTargetSummary, expected.expectedTargets, expected.maxPlacements));
391
+ if (!changedCounts || !targetAuditComplete) {
392
+ return {
393
+ status: "scheduler_receipt_incomplete",
394
+ schedulerStatus,
395
+ receipt: response?.receipt ?? null,
396
+ retryAfterMs: numberValue(response?.retryAfterMs),
397
+ changedCounts: null,
398
+ auditComplete: false,
399
+ blocker: "scheduler_receipt_incomplete",
400
+ };
401
+ }
402
+ return {
403
+ status: schedulerStatus,
404
+ // fix(112ao-2): the completion decision, carried explicitly instead of
405
+ // leaving each consumer to re-infer it from the status string.
406
+ terminal: schedulerEnvelopeIsTerminal(schedulerStatus, receipt),
407
+ receipt: response?.receipt ?? null,
408
+ retryAfterMs: numberValue(response?.retryAfterMs),
409
+ changedCounts,
410
+ auditComplete: true,
411
+ };
412
+ }