@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.
- package/dist/agent-tool-names.d.ts +2 -2
- package/dist/agent-tool-names.js +14 -0
- package/dist/scheduler-envelope.d.ts +190 -0
- package/dist/scheduler-envelope.js +412 -0
- package/dist/server.js +28 -2
- package/dist/tools/campaign-message-preparation.d.ts +25 -0
- package/dist/tools/campaign-message-preparation.js +37 -0
- package/dist/tools/campaign-processing.d.ts +74 -1
- package/dist/tools/campaign-processing.js +57 -1
- package/dist/tools/prompts.d.ts +15 -3
- package/dist/tools/prompts.js +25 -0
- package/dist/tools/refill-executors.d.ts +43 -70
- package/dist/tools/refill-executors.js +201 -273
- package/dist/tools/refill-sends.d.ts +3 -3
- package/dist/tools/refill-sends.js +1 -1
- package/dist/tools/refill-target-plan.js +1 -1
- package/dist/tools/refill-v3-advance-contract.d.ts +618 -0
- package/dist/tools/refill-v3-advance-contract.js +920 -0
- package/dist/tools/refill-v3-advance.d.ts +397 -0
- package/dist/tools/refill-v3-advance.js +75 -0
- package/dist/tools/refill-v3-attention-packet-contract.d.ts +608 -0
- package/dist/tools/refill-v3-attention-packet-contract.js +911 -0
- package/dist/tools/refill-v3-campaign-work.d.ts +324 -0
- package/dist/tools/refill-v3-campaign-work.js +1015 -0
- package/dist/tools/refill-v3-continue-contract.d.ts +433 -0
- package/dist/tools/refill-v3-continue-contract.js +112 -0
- package/dist/tools/refill-v3-continue.d.ts +501 -0
- package/dist/tools/refill-v3-continue.js +654 -0
- package/dist/tools/refill-v3-edit-selected-campaign-contract.d.ts +208 -0
- package/dist/tools/refill-v3-edit-selected-campaign-contract.js +351 -0
- package/dist/tools/refill-v3-edit-selected-campaign.d.ts +232 -0
- package/dist/tools/refill-v3-edit-selected-campaign.js +147 -0
- package/dist/tools/refill-v3-fill-ready-contract.d.ts +198 -0
- package/dist/tools/refill-v3-fill-ready-contract.js +324 -0
- package/dist/tools/refill-v3-fill-ready.d.ts +199 -0
- package/dist/tools/refill-v3-fill-ready.js +136 -0
- package/dist/tools/refill-v3-source-family-dispatch.d.ts +249 -0
- package/dist/tools/refill-v3-source-family-dispatch.js +514 -0
- package/dist/tools/refill-v3-waterfall-contract.d.ts +156 -0
- package/dist/tools/refill-v3-waterfall-contract.js +181 -0
- package/dist/tools/refill-v3-waterfall.d.ts +136 -0
- package/dist/tools/refill-v3-waterfall.js +161 -0
- package/dist/tools/refill-v3-world-state-contract.d.ts +666 -0
- package/dist/tools/refill-v3-world-state-contract.js +790 -0
- package/dist/tools/refill-v3-world-state.d.ts +592 -0
- package/dist/tools/refill-v3-world-state.js +137 -0
- package/dist/tools/registry.d.ts +1710 -3
- package/dist/tools/registry.js +19 -0
- package/package.json +1 -1
- package/skills/refill-sends/SKILL.md +89 -4
- package/skills/refill-sends-waterfall-order/SKILL.md +174 -0
- package/skills/refill-sends-work-campaign/SKILL.md +253 -0
- package/skills/refill-sends-workflow/SKILL.md +112 -5
- 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";
|
package/dist/agent-tool-names.js
CHANGED
|
@@ -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
|
+
}
|