@sellable/mcp 0.1.553 → 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.
- package/dist/refill-contract.d.ts +157 -0
- package/dist/refill-contract.js +487 -0
- package/dist/refill-run-client.d.ts +5 -0
- package/dist/refill-run-client.js +15 -0
- package/dist/refill-run-loop.d.ts +12 -1
- package/dist/refill-run-loop.js +158 -13
- package/dist/tools/campaign-message-preparation.d.ts +62 -0
- package/dist/tools/campaign-message-preparation.js +41 -0
- package/dist/tools/evergreen-refill-plan.d.ts +3 -0
- package/dist/tools/evergreen-refill-plan.js +29 -7
- package/dist/tools/prompts.js +9 -0
- package/dist/tools/refill-executors.d.ts +9 -1
- package/dist/tools/refill-executors.js +101 -8
- package/dist/tools/refill-sends-v2.d.ts +112 -1
- package/dist/tools/refill-sends-v2.js +302 -2
- package/dist/tools/refill-sends.d.ts +506 -2
- package/dist/tools/refill-sends.js +134 -1
- package/dist/tools/refill-target-plan.js +317 -23
- package/dist/tools/registry.d.ts +94 -33
- package/dist/tools/registry.js +1 -3
- package/dist/tools/scheduler-run.d.ts +61 -0
- package/dist/tools/scheduler-run.js +183 -1
- package/package.json +1 -1
- package/skills/refill-sends/SKILL.md +91 -388
- package/skills/refill-sends-v2/SKILL.md +6 -6
- package/skills/refill-sends-v2-workflow/SKILL.md +5 -5
- package/skills/refill-sends-v2-workflow/core/flow.v1.json +8 -8
- package/skills/refill-sends-workflow/SKILL.md +100 -781
- package/skills/refill-sends-workflow/core/contract.v2.json +543 -0
- package/skills/refill-sends-workflow/core/flow.v1.json +122 -0
|
@@ -3,7 +3,7 @@ import { startPrepareCampaignMessages } from "./campaign-message-preparation.js"
|
|
|
3
3
|
import { startCampaign } from "./campaigns.js";
|
|
4
4
|
import { confirmLeadList, importLeads, searchSignals, selectPromisingPosts, } from "./leads.js";
|
|
5
5
|
import { markProviderPromptLoaded } from "./provider-preflight.js";
|
|
6
|
-
import { runSchedulerSweep } from "./scheduler-run.js";
|
|
6
|
+
import { runSchedulerSweep, } from "./scheduler-run.js";
|
|
7
7
|
import { refreshPaidInmailCredits } from "./senders.js";
|
|
8
8
|
import { workspaceRequestOptions } from "./workspace-context.js";
|
|
9
9
|
const PAID_INMAIL_REFRESH_MAX_ATTEMPTS = 3;
|
|
@@ -86,19 +86,87 @@ export function normalizeSchedulerChangedCounts(value) {
|
|
|
86
86
|
if (groups.reduce((sum, group) => sum + group.count, 0) !== total) {
|
|
87
87
|
return null;
|
|
88
88
|
}
|
|
89
|
+
if (!Array.isArray(changedCounts.byCampaignTableActionTypeSender)) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const senderGroups = [];
|
|
93
|
+
const senderSeen = new Set();
|
|
94
|
+
for (const rawGroup of changedCounts.byCampaignTableActionTypeSender) {
|
|
95
|
+
const group = recordValue(rawGroup);
|
|
96
|
+
const campaignId = stringValue(group?.campaignId);
|
|
97
|
+
const tableId = stringValue(group?.tableId);
|
|
98
|
+
const actionType = stringValue(group?.actionType);
|
|
99
|
+
const senderId = stringValue(group?.senderId);
|
|
100
|
+
const count = numberValue(group?.count);
|
|
101
|
+
if (!campaignId ||
|
|
102
|
+
!tableId ||
|
|
103
|
+
!actionType ||
|
|
104
|
+
!senderId ||
|
|
105
|
+
count === null ||
|
|
106
|
+
count <= 0 ||
|
|
107
|
+
!Number.isInteger(count)) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const key = `${campaignId}:${tableId}:${actionType}:${senderId}`;
|
|
111
|
+
if (senderSeen.has(key))
|
|
112
|
+
return null;
|
|
113
|
+
senderSeen.add(key);
|
|
114
|
+
senderGroups.push({ campaignId, tableId, actionType, senderId, count });
|
|
115
|
+
}
|
|
116
|
+
if (senderGroups.reduce((sum, group) => sum + group.count, 0) !== total) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
89
119
|
return {
|
|
90
120
|
complete: true,
|
|
91
121
|
truncated: false,
|
|
92
122
|
total,
|
|
93
123
|
byCampaignTableActionType: groups,
|
|
124
|
+
byCampaignTableActionTypeSender: senderGroups,
|
|
94
125
|
};
|
|
95
126
|
}
|
|
96
|
-
|
|
127
|
+
function completeExpectedTargetSummary(value, expectedTargets, maxPlacements) {
|
|
128
|
+
const summary = recordValue(value);
|
|
129
|
+
const items = Array.isArray(summary?.items) ? summary.items : [];
|
|
130
|
+
if (summary?.complete !== true ||
|
|
131
|
+
summary.truncated !== false ||
|
|
132
|
+
numberValue(summary.requested) !== expectedTargets.length ||
|
|
133
|
+
numberValue(summary.included) == null ||
|
|
134
|
+
numberValue(summary.omitted) == null ||
|
|
135
|
+
numberValue(summary.included) + numberValue(summary.omitted) !==
|
|
136
|
+
expectedTargets.length ||
|
|
137
|
+
numberValue(summary.maxPlacements) !== maxPlacements ||
|
|
138
|
+
items.length !== expectedTargets.length) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
return items.every((rawItem, index) => {
|
|
142
|
+
const item = recordValue(rawItem);
|
|
143
|
+
const target = expectedTargets[index];
|
|
144
|
+
const senderIds = stringArray(item?.senderIds).sort();
|
|
145
|
+
const bySender = Array.isArray(item?.bySender) ? item.bySender : [];
|
|
146
|
+
const accounted = bySender
|
|
147
|
+
.map((entry) => stringValue(recordValue(entry)?.senderId))
|
|
148
|
+
.filter((senderId) => Boolean(senderId))
|
|
149
|
+
.sort();
|
|
150
|
+
return (Boolean(target) &&
|
|
151
|
+
item?.campaignId === target?.campaignId &&
|
|
152
|
+
item?.tableId === target?.tableId &&
|
|
153
|
+
item?.refillLaneKey === target?.refillLaneKey &&
|
|
154
|
+
(item?.outcome === "included" || item?.outcome === "omitted") &&
|
|
155
|
+
JSON.stringify(senderIds) === JSON.stringify(target?.senderIds) &&
|
|
156
|
+
JSON.stringify(accounted) === JSON.stringify(target?.senderIds));
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
export function normalizeSchedulerPrimitiveResult(value, expected) {
|
|
97
160
|
const response = recordValue(value);
|
|
98
161
|
const receipt = recordValue(response?.receipt);
|
|
99
162
|
const changedCounts = normalizeSchedulerChangedCounts(receipt?.changedCounts ?? response?.changedCounts);
|
|
163
|
+
const targetAuditComplete = !expected ||
|
|
164
|
+
(receipt?.receiptVersion === 3 &&
|
|
165
|
+
receipt.expectedTargetsHash === expected.expectedTargetsHash &&
|
|
166
|
+
receipt.maxPlacements === expected.maxPlacements &&
|
|
167
|
+
completeExpectedTargetSummary(receipt.expectedTargetSummary, expected.expectedTargets, expected.maxPlacements));
|
|
100
168
|
const schedulerStatus = stringValue(response?.status) ?? stringValue(receipt?.status) ?? "unknown";
|
|
101
|
-
if (!changedCounts) {
|
|
169
|
+
if (!changedCounts || !targetAuditComplete) {
|
|
102
170
|
return {
|
|
103
171
|
status: "scheduler_receipt_incomplete",
|
|
104
172
|
schedulerStatus,
|
|
@@ -312,7 +380,9 @@ export function classifyPaidInmailRefreshReceipt(value) {
|
|
|
312
380
|
refreshed,
|
|
313
381
|
error: stringValue(response?.error) ??
|
|
314
382
|
stringValue(nestedReceipt?.error) ??
|
|
315
|
-
(usableCurrentFacts
|
|
383
|
+
(usableCurrentFacts
|
|
384
|
+
? null
|
|
385
|
+
: "paid InMail credit refresh did not return usable current facts"),
|
|
316
386
|
};
|
|
317
387
|
}
|
|
318
388
|
export function firstGlobalAction(plan) {
|
|
@@ -856,7 +926,10 @@ export async function executeOneYoloPrimitive(action, workspaceId) {
|
|
|
856
926
|
case "wait_for_scheduler":
|
|
857
927
|
case "wait_for_active_work":
|
|
858
928
|
case "wait_for_source_import":
|
|
859
|
-
return {
|
|
929
|
+
return {
|
|
930
|
+
status: "read_only_reread",
|
|
931
|
+
result: waitResultForAction(action),
|
|
932
|
+
};
|
|
860
933
|
case "run_scheduler_sweep": {
|
|
861
934
|
const toolInput = actionToolInput(action);
|
|
862
935
|
const actionWorkspaceId = stringValue(toolInput.workspaceId);
|
|
@@ -867,6 +940,11 @@ export async function executeOneYoloPrimitive(action, workspaceId) {
|
|
|
867
940
|
const targetShapeRevision = stringValue(toolInput.targetShapeRevision);
|
|
868
941
|
const receiptRequirements = recordValue(action.receiptRequirements);
|
|
869
942
|
const receiptGroups = stringArray(receiptRequirements?.groupBy);
|
|
943
|
+
const expectedTargets = Array.isArray(toolInput.expectedTargets)
|
|
944
|
+
? toolInput.expectedTargets
|
|
945
|
+
: [];
|
|
946
|
+
const expectedTargetsHash = stringValue(toolInput.expectedTargetsHash);
|
|
947
|
+
const maxPlacements = numberValue(toolInput.maxPlacements);
|
|
870
948
|
if (!workspaceId ||
|
|
871
949
|
!actionWorkspaceId ||
|
|
872
950
|
actionWorkspaceId !== workspaceId ||
|
|
@@ -878,7 +956,13 @@ export async function executeOneYoloPrimitive(action, workspaceId) {
|
|
|
878
956
|
!targetShapeRevision ||
|
|
879
957
|
action.workspaceWide !== true ||
|
|
880
958
|
receiptRequirements?.nonTruncated !== true ||
|
|
881
|
-
receiptGroups.join(":") !== "campaignId:tableId:actionType"
|
|
959
|
+
receiptGroups.join(":") !== "campaignId:tableId:actionType:senderId" ||
|
|
960
|
+
receiptRequirements?.expectedTargetSummary !== true ||
|
|
961
|
+
expectedTargets.length === 0 ||
|
|
962
|
+
!expectedTargetsHash ||
|
|
963
|
+
maxPlacements === null ||
|
|
964
|
+
!Number.isInteger(maxPlacements) ||
|
|
965
|
+
maxPlacements <= 0) {
|
|
882
966
|
return {
|
|
883
967
|
status: "refused",
|
|
884
968
|
refusalReason: "run_scheduler_sweep action is missing exact workspace/date/revision scope or complete receipt requirements",
|
|
@@ -890,10 +974,17 @@ export async function executeOneYoloPrimitive(action, workspaceId) {
|
|
|
890
974
|
targetDate,
|
|
891
975
|
requestKey,
|
|
892
976
|
targetShapeRevision,
|
|
977
|
+
expectedTargets,
|
|
978
|
+
expectedTargetsHash,
|
|
979
|
+
maxPlacements,
|
|
893
980
|
});
|
|
894
981
|
return {
|
|
895
982
|
status: "executed_and_reread",
|
|
896
|
-
result: normalizeSchedulerPrimitiveResult(result
|
|
983
|
+
result: normalizeSchedulerPrimitiveResult(result, {
|
|
984
|
+
expectedTargets,
|
|
985
|
+
expectedTargetsHash,
|
|
986
|
+
maxPlacements,
|
|
987
|
+
}),
|
|
897
988
|
};
|
|
898
989
|
}
|
|
899
990
|
case "prepare_messages": {
|
|
@@ -917,7 +1008,9 @@ export async function executeOneYoloPrimitive(action, workspaceId) {
|
|
|
917
1008
|
tableId,
|
|
918
1009
|
...(workspaceId ? { workspaceId } : {}),
|
|
919
1010
|
targetPreparedMessages,
|
|
920
|
-
|
|
1011
|
+
...(numberValue(toolInput.maxRowsToCheck) != null
|
|
1012
|
+
? { maxRowsToCheck: numberValue(toolInput.maxRowsToCheck) }
|
|
1013
|
+
: {}),
|
|
921
1014
|
approvalMode,
|
|
922
1015
|
rowSelector,
|
|
923
1016
|
...(excludeRowIds.length > 0 ? { excludeRowIds } : {}),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
type RefillSendsV2Input = {
|
|
1
|
+
export type RefillSendsV2Input = {
|
|
2
2
|
workspaceId?: string;
|
|
3
3
|
runId?: string;
|
|
4
4
|
fence?: number;
|
|
@@ -6,6 +6,39 @@ type RefillSendsV2Input = {
|
|
|
6
6
|
approvalMode?: "approve" | "mark_ready";
|
|
7
7
|
senderIds?: string[];
|
|
8
8
|
intent?: "auto" | "evergreen" | "plain" | "active";
|
|
9
|
+
targetDate?: string;
|
|
10
|
+
untilDate?: string;
|
|
11
|
+
horizonSendDays?: number;
|
|
12
|
+
scope?: Record<string, unknown>;
|
|
13
|
+
scopeHash?: string;
|
|
14
|
+
approval?: Record<string, unknown>;
|
|
15
|
+
targetPlan?: Record<string, unknown>;
|
|
16
|
+
approvalEcho?: {
|
|
17
|
+
scopeHash: string;
|
|
18
|
+
targetShapeRevision: string;
|
|
19
|
+
actionFingerprint: string;
|
|
20
|
+
expectedTargetsHash: string;
|
|
21
|
+
approvalExpiresAt: string;
|
|
22
|
+
approvalFingerprint: string;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
type RefillV2ApprovalPreparationInput = Pick<RefillSendsV2Input, "workspaceId" | "intent" | "senderIds" | "approvalMode" | "targetDate" | "untilDate" | "horizonSendDays"> & {
|
|
26
|
+
targetPlan: Record<string, unknown>;
|
|
27
|
+
now?: Date;
|
|
28
|
+
approvalExpiresAt?: string;
|
|
29
|
+
};
|
|
30
|
+
export declare const REFILL_V2_RUNTIME_IDENTITY: {
|
|
31
|
+
readonly contract: {
|
|
32
|
+
readonly version: "2.0.0";
|
|
33
|
+
readonly hash: string;
|
|
34
|
+
readonly cacheKey: "refill-contract-v1";
|
|
35
|
+
};
|
|
36
|
+
readonly installed: {
|
|
37
|
+
readonly mcpVersion: "phase98-local";
|
|
38
|
+
readonly installVersion: "phase98-local";
|
|
39
|
+
readonly pluginVersion: "phase98-local";
|
|
40
|
+
readonly cacheIdentity: "phase98-local-plugin-cache";
|
|
41
|
+
};
|
|
9
42
|
};
|
|
10
43
|
export declare const refillSendsV2ToolDefinitions: {
|
|
11
44
|
name: string;
|
|
@@ -44,11 +77,89 @@ export declare const refillSendsV2ToolDefinitions: {
|
|
|
44
77
|
type: string;
|
|
45
78
|
enum: string[];
|
|
46
79
|
};
|
|
80
|
+
targetDate: {
|
|
81
|
+
type: string;
|
|
82
|
+
description: string;
|
|
83
|
+
};
|
|
84
|
+
untilDate: {
|
|
85
|
+
type: string;
|
|
86
|
+
description: string;
|
|
87
|
+
};
|
|
88
|
+
horizonSendDays: {
|
|
89
|
+
type: string;
|
|
90
|
+
description: string;
|
|
91
|
+
};
|
|
47
92
|
};
|
|
48
93
|
required: string[];
|
|
49
94
|
additionalProperties: boolean;
|
|
50
95
|
};
|
|
51
96
|
}[];
|
|
97
|
+
export declare function prepareRefillSendsV2Approval(input: RefillV2ApprovalPreparationInput): Promise<{
|
|
98
|
+
status: "blocked";
|
|
99
|
+
blocker: string;
|
|
100
|
+
response: unknown;
|
|
101
|
+
} | {
|
|
102
|
+
status: "awaiting_approval";
|
|
103
|
+
runId: string;
|
|
104
|
+
fence: number;
|
|
105
|
+
scope: Record<string, unknown> | {
|
|
106
|
+
version: 2;
|
|
107
|
+
workspaceId: string;
|
|
108
|
+
intent: "active" | "plain" | "evergreen" | "auto";
|
|
109
|
+
senderIds: string[];
|
|
110
|
+
campaignIds: string[];
|
|
111
|
+
tableIds: string[];
|
|
112
|
+
actionIds: string[];
|
|
113
|
+
approvalMode: "mark_ready" | "approve";
|
|
114
|
+
dateSelector: {
|
|
115
|
+
kind: "target_date";
|
|
116
|
+
targetDate: string;
|
|
117
|
+
untilDate?: undefined;
|
|
118
|
+
horizonSendDays?: undefined;
|
|
119
|
+
} | {
|
|
120
|
+
kind: "until_date";
|
|
121
|
+
untilDate: string;
|
|
122
|
+
targetDate?: undefined;
|
|
123
|
+
horizonSendDays?: undefined;
|
|
124
|
+
} | {
|
|
125
|
+
kind: "horizon_send_days";
|
|
126
|
+
horizonSendDays: number;
|
|
127
|
+
targetDate?: undefined;
|
|
128
|
+
untilDate?: undefined;
|
|
129
|
+
} | {
|
|
130
|
+
kind: "scheduler_default";
|
|
131
|
+
targetDate?: undefined;
|
|
132
|
+
untilDate?: undefined;
|
|
133
|
+
horizonSendDays?: undefined;
|
|
134
|
+
};
|
|
135
|
+
senderTimezones: {
|
|
136
|
+
[k: string]: string;
|
|
137
|
+
};
|
|
138
|
+
expectedTargetsHash: string;
|
|
139
|
+
maxPlacements: number;
|
|
140
|
+
contract: {
|
|
141
|
+
version: "2.0.0";
|
|
142
|
+
hash: string;
|
|
143
|
+
cacheKey: "refill-contract-v1";
|
|
144
|
+
};
|
|
145
|
+
installed: {
|
|
146
|
+
mcpVersion: "phase98-local";
|
|
147
|
+
installVersion: "phase98-local";
|
|
148
|
+
pluginVersion: "phase98-local";
|
|
149
|
+
cacheIdentity: "phase98-local-plugin-cache";
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
approvalFingerprint: string;
|
|
153
|
+
scopeHash: string;
|
|
154
|
+
targetShapeRevision: string;
|
|
155
|
+
actionFingerprint: string;
|
|
156
|
+
expectedTargetsHash: string;
|
|
157
|
+
sideEffectEnvelope: string[];
|
|
158
|
+
maxPlacements: number;
|
|
159
|
+
approvalExpiresAt: string;
|
|
160
|
+
blocker?: undefined;
|
|
161
|
+
response?: undefined;
|
|
162
|
+
}>;
|
|
52
163
|
export declare function refillSendsV2Command(input: RefillSendsV2Input): Promise<{
|
|
53
164
|
readOnly: boolean;
|
|
54
165
|
mode: string;
|
|
@@ -1,11 +1,26 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { REFILL_CONTRACT_CACHE_VERSION, REFILL_CONTRACT_HASH, REFILL_CONTRACT_VERSION, renderRefillSafetyGuidance, } from "../refill-contract.js";
|
|
1
3
|
import { appendIndexLine, appendJournalEvent, createRunJournal, renderBootstrapSection, renderCrashedAbandonedSection, renderExecutionEventSection, renderPlanSection, renderTerminalSection, } from "../refill-journal.js";
|
|
2
4
|
import { writeRefillWorkspaceState } from "../refill-local-state.js";
|
|
3
5
|
import { advanceRefillRunGateRemote, completeRefillRun, heartbeatRefillRun, recordRunOutcome, recordRunPlanned, refillRunStatus, startRefillRunRemote, } from "../refill-run-client.js";
|
|
4
6
|
import { runRefillV2Loop } from "../refill-run-loop.js";
|
|
5
|
-
import { getPrepareCampaignMessagesStatus } from "./campaign-message-preparation.js";
|
|
7
|
+
import { getPrepareCampaignMessagesStatus, stopSatisfiedPrepareCampaignMessages, } from "./campaign-message-preparation.js";
|
|
6
8
|
import { getRefillPlanV2 } from "./evergreen-refill-plan.js";
|
|
7
9
|
import { executeOneYoloPrimitive, executeStartCampaignPrimitive, prepareRowSelectorValue, refillPrepareRequestHash, refreshPaidInmailCreditsWithRetry, } from "./refill-executors.js";
|
|
8
10
|
import { runSchedulerSweep } from "./scheduler-run.js";
|
|
11
|
+
export const REFILL_V2_RUNTIME_IDENTITY = {
|
|
12
|
+
contract: {
|
|
13
|
+
version: REFILL_CONTRACT_VERSION,
|
|
14
|
+
hash: REFILL_CONTRACT_HASH,
|
|
15
|
+
cacheKey: REFILL_CONTRACT_CACHE_VERSION,
|
|
16
|
+
},
|
|
17
|
+
installed: {
|
|
18
|
+
mcpVersion: "phase98-local",
|
|
19
|
+
installVersion: "phase98-local",
|
|
20
|
+
pluginVersion: "phase98-local",
|
|
21
|
+
cacheIdentity: "phase98-local-plugin-cache",
|
|
22
|
+
},
|
|
23
|
+
};
|
|
9
24
|
const FORBIDDEN_ACTIONS = [
|
|
10
25
|
"Do not schedule sends.",
|
|
11
26
|
"Do not send messages.",
|
|
@@ -18,7 +33,11 @@ const BOUNDED_AUTHORITY = "Within the refill run, bounded approvals, preparation
|
|
|
18
33
|
export const refillSendsV2ToolDefinitions = [
|
|
19
34
|
{
|
|
20
35
|
name: "refill_sends_v2",
|
|
21
|
-
description:
|
|
36
|
+
description: [
|
|
37
|
+
"Execute the server-internal refill loop behind public refill_sends; it is retained for dispatch compatibility but is not advertised.",
|
|
38
|
+
`contractHash=${REFILL_CONTRACT_HASH}`,
|
|
39
|
+
renderRefillSafetyGuidance(),
|
|
40
|
+
].join(" "),
|
|
22
41
|
inputSchema: {
|
|
23
42
|
type: "object",
|
|
24
43
|
properties: {
|
|
@@ -51,6 +70,18 @@ export const refillSendsV2ToolDefinitions = [
|
|
|
51
70
|
type: "string",
|
|
52
71
|
enum: ["auto", "evergreen", "plain", "active"],
|
|
53
72
|
},
|
|
73
|
+
targetDate: {
|
|
74
|
+
type: "string",
|
|
75
|
+
description: "Exact sender-local YYYY-MM-DD date.",
|
|
76
|
+
},
|
|
77
|
+
untilDate: {
|
|
78
|
+
type: "string",
|
|
79
|
+
description: "Inclusive sender-local YYYY-MM-DD through-date.",
|
|
80
|
+
},
|
|
81
|
+
horizonSendDays: {
|
|
82
|
+
type: "number",
|
|
83
|
+
description: "Positive sender-local send-day horizon.",
|
|
84
|
+
},
|
|
54
85
|
},
|
|
55
86
|
required: ["workspaceId"],
|
|
56
87
|
additionalProperties: false,
|
|
@@ -68,6 +99,234 @@ function resumeHandle(input) {
|
|
|
68
99
|
? { runId: input.runId, fence: input.fence }
|
|
69
100
|
: undefined;
|
|
70
101
|
}
|
|
102
|
+
function validDate(value, field) {
|
|
103
|
+
const trimmed = value.trim();
|
|
104
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(trimmed) ||
|
|
105
|
+
new Date(`${trimmed}T00:00:00.000Z`).toISOString().slice(0, 10) !== trimmed) {
|
|
106
|
+
throw new Error(`${field} must be a valid YYYY-MM-DD date.`);
|
|
107
|
+
}
|
|
108
|
+
return trimmed;
|
|
109
|
+
}
|
|
110
|
+
function normalizeDateSelector(input) {
|
|
111
|
+
const selected = [
|
|
112
|
+
input.targetDate !== undefined,
|
|
113
|
+
input.untilDate !== undefined,
|
|
114
|
+
input.horizonSendDays !== undefined,
|
|
115
|
+
].filter(Boolean).length;
|
|
116
|
+
if (selected > 1) {
|
|
117
|
+
throw new Error("Provide exactly one refill date selector.");
|
|
118
|
+
}
|
|
119
|
+
if (input.targetDate !== undefined) {
|
|
120
|
+
const targetDate = validDate(input.targetDate, "targetDate");
|
|
121
|
+
return {
|
|
122
|
+
dateSelector: { kind: "target_date", targetDate },
|
|
123
|
+
targetDate,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (input.untilDate !== undefined) {
|
|
127
|
+
const untilDate = validDate(input.untilDate, "untilDate");
|
|
128
|
+
return {
|
|
129
|
+
dateSelector: { kind: "until_date", untilDate },
|
|
130
|
+
untilDate,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (input.horizonSendDays !== undefined) {
|
|
134
|
+
if (!Number.isInteger(input.horizonSendDays) ||
|
|
135
|
+
input.horizonSendDays <= 0) {
|
|
136
|
+
throw new Error("horizonSendDays must be a positive integer.");
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
dateSelector: {
|
|
140
|
+
kind: "horizon_send_days",
|
|
141
|
+
horizonSendDays: input.horizonSendDays,
|
|
142
|
+
},
|
|
143
|
+
horizonSendDays: input.horizonSendDays,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return { dateSelector: { kind: "scheduler_default" } };
|
|
147
|
+
}
|
|
148
|
+
function record(value) {
|
|
149
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
150
|
+
? value
|
|
151
|
+
: null;
|
|
152
|
+
}
|
|
153
|
+
function text(value) {
|
|
154
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
155
|
+
}
|
|
156
|
+
function list(value) {
|
|
157
|
+
return Array.isArray(value) ? value : [];
|
|
158
|
+
}
|
|
159
|
+
function sortedUnique(values) {
|
|
160
|
+
return [
|
|
161
|
+
...new Set(values.filter((value) => Boolean(value))),
|
|
162
|
+
].sort((left, right) => left.localeCompare(right));
|
|
163
|
+
}
|
|
164
|
+
function sha256(value) {
|
|
165
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
166
|
+
}
|
|
167
|
+
function targetPlanParts(targetPlan) {
|
|
168
|
+
const target = record(targetPlan.target) ?? {};
|
|
169
|
+
const plans = list(target.senderRefillPlans)
|
|
170
|
+
.map(record)
|
|
171
|
+
.filter((value) => value != null);
|
|
172
|
+
const actions = list(target.globalActionQueue)
|
|
173
|
+
.map(record)
|
|
174
|
+
.filter((value) => value != null);
|
|
175
|
+
return { target, plans, actions };
|
|
176
|
+
}
|
|
177
|
+
function stableIdsFromTargetPlan(targetPlan, input) {
|
|
178
|
+
const { plans, actions } = targetPlanParts(targetPlan);
|
|
179
|
+
const actionRecords = actions.map((action) => ({
|
|
180
|
+
action,
|
|
181
|
+
ids: record(action.ids) ?? {},
|
|
182
|
+
toolInput: record(action.toolInput) ?? {},
|
|
183
|
+
}));
|
|
184
|
+
const senderIds = sortedUnique([
|
|
185
|
+
...(input.senderIds ?? []),
|
|
186
|
+
...plans.map((plan) => text(plan.senderId)),
|
|
187
|
+
...actionRecords.map(({ ids, toolInput }) => text(ids.senderId) ?? text(toolInput.senderId)),
|
|
188
|
+
]);
|
|
189
|
+
const campaignIds = sortedUnique(actionRecords.map(({ ids, toolInput }) => text(ids.campaignId) ?? text(toolInput.campaignId)));
|
|
190
|
+
const tableIds = sortedUnique(actionRecords.map(({ ids, toolInput }) => text(ids.tableId) ?? text(toolInput.tableId)));
|
|
191
|
+
const actionIds = sortedUnique(actionRecords.map(({ action }) => text(action.actionKey) ?? text(action.type)));
|
|
192
|
+
const senderTimezones = Object.fromEntries(plans
|
|
193
|
+
.flatMap((plan) => list(plan.selectedDays))
|
|
194
|
+
.map(record)
|
|
195
|
+
.filter((day) => day != null)
|
|
196
|
+
.map((day) => [
|
|
197
|
+
text(day.senderId),
|
|
198
|
+
text(day.senderTimeZone) ?? text(day.timeZone),
|
|
199
|
+
])
|
|
200
|
+
.filter((entry) => entry[0] != null && entry[1] != null)
|
|
201
|
+
.sort(([left], [right]) => left.localeCompare(right)));
|
|
202
|
+
return {
|
|
203
|
+
senderIds,
|
|
204
|
+
campaignIds,
|
|
205
|
+
tableIds,
|
|
206
|
+
actionIds,
|
|
207
|
+
senderTimezones,
|
|
208
|
+
actions: actionRecords,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function buildApprovalEnvelope(input) {
|
|
212
|
+
const workspaceId = normalizeWorkspaceId(input.workspaceId);
|
|
213
|
+
const date = normalizeDateSelector(input);
|
|
214
|
+
const stable = stableIdsFromTargetPlan(input.targetPlan, input);
|
|
215
|
+
const schedulerAction = stable.actions.find(({ action }) => action.type === "run_scheduler_sweep");
|
|
216
|
+
const schedulerInput = schedulerAction?.toolInput ?? {};
|
|
217
|
+
const expectedTargets = list(schedulerInput.expectedTargets);
|
|
218
|
+
const expectedTargetsHash = text(schedulerInput.expectedTargetsHash) ?? sha256(expectedTargets);
|
|
219
|
+
const maxPlacements = typeof schedulerInput.maxPlacements === "number" &&
|
|
220
|
+
Number.isInteger(schedulerInput.maxPlacements) &&
|
|
221
|
+
schedulerInput.maxPlacements > 0
|
|
222
|
+
? schedulerInput.maxPlacements
|
|
223
|
+
: 150;
|
|
224
|
+
const scope = {
|
|
225
|
+
version: 2,
|
|
226
|
+
workspaceId,
|
|
227
|
+
intent: input.intent ?? "auto",
|
|
228
|
+
senderIds: stable.senderIds,
|
|
229
|
+
campaignIds: stable.campaignIds,
|
|
230
|
+
tableIds: stable.tableIds,
|
|
231
|
+
actionIds: stable.actionIds,
|
|
232
|
+
approvalMode: input.approvalMode ?? "approve",
|
|
233
|
+
dateSelector: date.dateSelector,
|
|
234
|
+
senderTimezones: stable.senderTimezones,
|
|
235
|
+
expectedTargetsHash,
|
|
236
|
+
maxPlacements,
|
|
237
|
+
contract: { ...REFILL_V2_RUNTIME_IDENTITY.contract },
|
|
238
|
+
installed: { ...REFILL_V2_RUNTIME_IDENTITY.installed },
|
|
239
|
+
};
|
|
240
|
+
const scopeHash = sha256(scope);
|
|
241
|
+
const targetShapeRevision = text(input.targetPlan.targetShapeRevision) ??
|
|
242
|
+
text(record(input.targetPlan.target)?.targetShapeRevision) ??
|
|
243
|
+
"missing_target_shape_revision";
|
|
244
|
+
const actionFingerprint = text(stable.actions[0]?.action.actionKey) ??
|
|
245
|
+
text(stable.actions[0]?.action.type) ??
|
|
246
|
+
"no_action";
|
|
247
|
+
const sideEffectEnvelope = sortedUnique(stable.actions.map(({ action }) => text(action.type)));
|
|
248
|
+
const approvalExpiresAt = input.approvalExpiresAt ??
|
|
249
|
+
new Date((input.now ?? new Date()).getTime() + 15 * 60_000).toISOString();
|
|
250
|
+
const fingerprintFields = {
|
|
251
|
+
scopeHash,
|
|
252
|
+
targetShapeRevision,
|
|
253
|
+
actionFingerprint,
|
|
254
|
+
expectedTargetsHash,
|
|
255
|
+
sideEffectEnvelope,
|
|
256
|
+
maxPlacements,
|
|
257
|
+
approvalExpiresAt,
|
|
258
|
+
};
|
|
259
|
+
const approval = {
|
|
260
|
+
status: "awaiting_approval",
|
|
261
|
+
...fingerprintFields,
|
|
262
|
+
approvalFingerprint: sha256(fingerprintFields),
|
|
263
|
+
};
|
|
264
|
+
return { scope, scopeHash, approval, date };
|
|
265
|
+
}
|
|
266
|
+
export async function prepareRefillSendsV2Approval(input) {
|
|
267
|
+
const envelope = buildApprovalEnvelope(input);
|
|
268
|
+
const started = await startRefillRunRemote({
|
|
269
|
+
workspaceId: envelope.scope.workspaceId,
|
|
270
|
+
config: {
|
|
271
|
+
scope: envelope.scope,
|
|
272
|
+
scopeHash: envelope.scopeHash,
|
|
273
|
+
approval: envelope.approval,
|
|
274
|
+
},
|
|
275
|
+
runState: {
|
|
276
|
+
version: 1,
|
|
277
|
+
scope: envelope.scope,
|
|
278
|
+
scopeHash: envelope.scopeHash,
|
|
279
|
+
approval: envelope.approval,
|
|
280
|
+
senderCursors: [],
|
|
281
|
+
laneCooldowns: [],
|
|
282
|
+
passRates: [],
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
const result = record(started) ?? {};
|
|
286
|
+
const runId = text(result.runId);
|
|
287
|
+
const fence = typeof result.fence === "number" && Number.isFinite(result.fence)
|
|
288
|
+
? result.fence
|
|
289
|
+
: null;
|
|
290
|
+
if (!runId || fence == null) {
|
|
291
|
+
return {
|
|
292
|
+
status: "blocked",
|
|
293
|
+
blocker: text(result.blocker) ?? "approval_run_unavailable",
|
|
294
|
+
response: started,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const persistedConfig = record(result.config);
|
|
298
|
+
const persistedScope = record(persistedConfig?.scope) ?? envelope.scope;
|
|
299
|
+
const persistedApproval = record(persistedConfig?.approval) ??
|
|
300
|
+
envelope.approval;
|
|
301
|
+
return {
|
|
302
|
+
...persistedApproval,
|
|
303
|
+
status: "awaiting_approval",
|
|
304
|
+
runId,
|
|
305
|
+
fence,
|
|
306
|
+
scope: persistedScope,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function validateApprovalEcho(envelope, echo, now = new Date()) {
|
|
310
|
+
const fields = [
|
|
311
|
+
"scopeHash",
|
|
312
|
+
"targetShapeRevision",
|
|
313
|
+
"actionFingerprint",
|
|
314
|
+
"expectedTargetsHash",
|
|
315
|
+
"approvalExpiresAt",
|
|
316
|
+
"approvalFingerprint",
|
|
317
|
+
];
|
|
318
|
+
const changed = fields.filter((field) => echo[field] !== envelope.approval[field]);
|
|
319
|
+
if (Date.parse(envelope.approval.approvalExpiresAt) <= now.getTime()) {
|
|
320
|
+
return { ok: false, blocker: "approval_expired", changed };
|
|
321
|
+
}
|
|
322
|
+
return changed.length === 0
|
|
323
|
+
? { ok: true }
|
|
324
|
+
: {
|
|
325
|
+
ok: false,
|
|
326
|
+
blocker: "approval_fingerprint_mismatch",
|
|
327
|
+
changed,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
71
330
|
async function maybeAddLostFenceGuidance(result, input) {
|
|
72
331
|
if (result.status !== "blocked" ||
|
|
73
332
|
result.blocker !== "lease_lost" ||
|
|
@@ -94,12 +353,45 @@ async function maybeAddLostFenceGuidance(result, input) {
|
|
|
94
353
|
}
|
|
95
354
|
export async function refillSendsV2Command(input) {
|
|
96
355
|
const workspaceId = normalizeWorkspaceId(input.workspaceId);
|
|
356
|
+
const date = normalizeDateSelector(input);
|
|
357
|
+
let resolvedScope = input.scope;
|
|
358
|
+
let resolvedScopeHash = input.scopeHash;
|
|
359
|
+
let resolvedApproval = input.approval;
|
|
360
|
+
if (input.targetPlan && input.approvalEcho) {
|
|
361
|
+
const envelope = buildApprovalEnvelope({
|
|
362
|
+
workspaceId,
|
|
363
|
+
intent: input.intent,
|
|
364
|
+
senderIds: input.senderIds,
|
|
365
|
+
approvalMode: input.approvalMode,
|
|
366
|
+
targetDate: input.targetDate,
|
|
367
|
+
untilDate: input.untilDate,
|
|
368
|
+
horizonSendDays: input.horizonSendDays,
|
|
369
|
+
targetPlan: input.targetPlan,
|
|
370
|
+
approvalExpiresAt: input.approvalEcho.approvalExpiresAt,
|
|
371
|
+
});
|
|
372
|
+
const validation = validateApprovalEcho(envelope, input.approvalEcho);
|
|
373
|
+
if (!validation.ok) {
|
|
374
|
+
return {
|
|
375
|
+
status: "blocked",
|
|
376
|
+
blocker: validation.blocker,
|
|
377
|
+
guidance: "The durable refill approval no longer matches fresh scope; render and approve a new packet before mutation.",
|
|
378
|
+
report: { changed: validation.changed, fresh: envelope.approval },
|
|
379
|
+
readOnly: false,
|
|
380
|
+
forbiddenActions: FORBIDDEN_ACTIONS,
|
|
381
|
+
boundedAuthority: BOUNDED_AUTHORITY,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
resolvedScope = envelope.scope;
|
|
385
|
+
resolvedScopeHash = envelope.scopeHash;
|
|
386
|
+
resolvedApproval = envelope.approval;
|
|
387
|
+
}
|
|
97
388
|
if (input.dryRun) {
|
|
98
389
|
const plan = await getRefillPlanV2({
|
|
99
390
|
workspaceId,
|
|
100
391
|
intent: input.intent,
|
|
101
392
|
senderIds: input.senderIds,
|
|
102
393
|
journal: true,
|
|
394
|
+
...date,
|
|
103
395
|
});
|
|
104
396
|
return {
|
|
105
397
|
...plan,
|
|
@@ -115,6 +407,10 @@ export async function refillSendsV2Command(input) {
|
|
|
115
407
|
senderIds: input.senderIds,
|
|
116
408
|
approvalMode: input.approvalMode ?? "approve",
|
|
117
409
|
resume: resumeHandle(input),
|
|
410
|
+
...date,
|
|
411
|
+
scope: resolvedScope,
|
|
412
|
+
scopeHash: resolvedScopeHash,
|
|
413
|
+
approval: resolvedApproval,
|
|
118
414
|
}, {
|
|
119
415
|
runClient: {
|
|
120
416
|
startRefillRunRemote,
|
|
@@ -131,12 +427,16 @@ export async function refillSendsV2Command(input) {
|
|
|
131
427
|
senderIds: planInput.senderIds,
|
|
132
428
|
runState: planInput.runState,
|
|
133
429
|
journal: planInput.journal,
|
|
430
|
+
targetDate: planInput.targetDate,
|
|
431
|
+
untilDate: planInput.untilDate,
|
|
432
|
+
horizonSendDays: planInput.horizonSendDays,
|
|
134
433
|
}),
|
|
135
434
|
executors: {
|
|
136
435
|
executeOneYoloPrimitive,
|
|
137
436
|
executeStartCampaignPrimitive,
|
|
138
437
|
refreshPaidInmailCreditsWithRetry,
|
|
139
438
|
getPrepareCampaignMessagesStatus,
|
|
439
|
+
stopSatisfiedPrepareCampaignMessages,
|
|
140
440
|
refillPrepareRequestHash,
|
|
141
441
|
prepareRowSelectorValue,
|
|
142
442
|
},
|