@sellable/mcp 0.1.739 → 0.1.741
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.
|
@@ -214,18 +214,21 @@ export type SchedulerChangedCounts = {
|
|
|
214
214
|
}>;
|
|
215
215
|
};
|
|
216
216
|
export declare function normalizeSchedulerChangedCounts(value: unknown): SchedulerChangedCounts | null;
|
|
217
|
+
export declare function schedulerEnvelopeIsTerminal(schedulerStatus: string): boolean;
|
|
217
218
|
export declare function normalizeSchedulerPrimitiveResult(value: unknown, expected?: {
|
|
218
219
|
expectedTargets: SchedulerExpectedTargetInput[];
|
|
219
220
|
expectedTargetsHash: string;
|
|
220
221
|
maxPlacements: number;
|
|
221
222
|
}): {
|
|
223
|
+
retryAfterMs: number;
|
|
224
|
+
changedCounts: null;
|
|
225
|
+
auditComplete: false;
|
|
226
|
+
replayedReceipt?: Record<string, unknown> | undefined;
|
|
222
227
|
status: "matching_scheduler_active";
|
|
223
228
|
schedulerStatus: string;
|
|
229
|
+
terminal: false;
|
|
224
230
|
disposition: "bounded_reread_wait";
|
|
225
231
|
receipt: null;
|
|
226
|
-
retryAfterMs: number;
|
|
227
|
-
changedCounts: null;
|
|
228
|
-
auditComplete: false;
|
|
229
232
|
blocker?: undefined;
|
|
230
233
|
} | {
|
|
231
234
|
status: string;
|
|
@@ -236,6 +239,7 @@ export declare function normalizeSchedulerPrimitiveResult(value: unknown, expect
|
|
|
236
239
|
changedCounts: null;
|
|
237
240
|
auditComplete: false;
|
|
238
241
|
blocker: string;
|
|
242
|
+
terminal?: undefined;
|
|
239
243
|
} | {
|
|
240
244
|
status: "scheduler_receipt_incomplete";
|
|
241
245
|
schedulerStatus: string;
|
|
@@ -245,8 +249,10 @@ export declare function normalizeSchedulerPrimitiveResult(value: unknown, expect
|
|
|
245
249
|
auditComplete: false;
|
|
246
250
|
blocker: "scheduler_receipt_incomplete";
|
|
247
251
|
disposition?: undefined;
|
|
252
|
+
terminal?: undefined;
|
|
248
253
|
} | {
|
|
249
254
|
status: string;
|
|
255
|
+
terminal: boolean;
|
|
250
256
|
receipt: {} | null;
|
|
251
257
|
retryAfterMs: number | null;
|
|
252
258
|
changedCounts: SchedulerChangedCounts;
|
|
@@ -274,18 +274,50 @@ function completeExpectedTargetSummary(value, expectedTargets, maxPlacements) {
|
|
|
274
274
|
JSON.stringify(accounted) === JSON.stringify(target?.senderIds));
|
|
275
275
|
});
|
|
276
276
|
}
|
|
277
|
+
// fix(112ao-2): the scheduler's in-flight envelope statuses. A response with
|
|
278
|
+
// one of these describes a job that is still running — it never describes an
|
|
279
|
+
// outcome of this dispatch, no matter what it carries alongside.
|
|
280
|
+
const SCHEDULER_IN_FLIGHT_ENVELOPE_STATUSES = new Set(["accepted", "attached"]);
|
|
281
|
+
// The envelope statuses that report a real outcome for this dispatch and are
|
|
282
|
+
// therefore eligible for the receipt audit below.
|
|
283
|
+
const SCHEDULER_TERMINAL_ENVELOPE_STATUSES = new Set([
|
|
284
|
+
"ran",
|
|
285
|
+
"window_closed_noop",
|
|
286
|
+
]);
|
|
287
|
+
export function schedulerEnvelopeIsTerminal(schedulerStatus) {
|
|
288
|
+
return SCHEDULER_TERMINAL_ENVELOPE_STATUSES.has(schedulerStatus);
|
|
289
|
+
}
|
|
277
290
|
export function normalizeSchedulerPrimitiveResult(value, expected) {
|
|
278
291
|
const response = recordValue(value);
|
|
279
292
|
const receipt = recordValue(response?.receipt);
|
|
280
293
|
const schedulerStatus = stringValue(response?.status) ?? stringValue(receipt?.status) ?? "unknown";
|
|
281
294
|
const errorCode = stringValue(recordValue(response?.error)?.code);
|
|
282
|
-
|
|
283
|
-
|
|
295
|
+
// fix(112ao-2): whether this dispatch finished is a total function of the
|
|
296
|
+
// ENVELOPE status, decided here before any receipt is read. The previous
|
|
297
|
+
// guard was `!receipt && (accepted || attached)`, which made completion
|
|
298
|
+
// depend on whether a field happened to be present: the scheduler may attach
|
|
299
|
+
// an in-flight response to an already-running job and replay THAT job's
|
|
300
|
+
// receipt, so an `attached` envelope arrived carrying complete, v3-auditable
|
|
301
|
+
// counters from a prior run. It skipped this branch, passed the audit on
|
|
302
|
+
// that stale evidence, and returned auditComplete: true — the coordinator
|
|
303
|
+
// read a finished sweep and redispatched the run it should have awaited
|
|
304
|
+
// (Dotwork 2026-07-29: 21 exact runs, coverage frozen at sent:0/scheduled:0).
|
|
305
|
+
//
|
|
306
|
+
// `accepted`/`attached` are IN-FLIGHT; `ran`/`window_closed_noop` are
|
|
307
|
+
// TERMINAL and continue to the audit below. `backoff` and `failed` keep their
|
|
308
|
+
// existing handling — the run loop already unwraps a backoff envelope's
|
|
309
|
+
// replayed receipt deliberately (schedulerRunReceiptIsFreshZeroScheduled).
|
|
310
|
+
if (SCHEDULER_IN_FLIGHT_ENVELOPE_STATUSES.has(schedulerStatus)) {
|
|
284
311
|
return {
|
|
285
312
|
status: "matching_scheduler_active",
|
|
286
313
|
schedulerStatus,
|
|
314
|
+
terminal: false,
|
|
287
315
|
disposition: "bounded_reread_wait",
|
|
288
316
|
receipt: null,
|
|
317
|
+
// A prior run's counters are never this dispatch's effect. Surfaced
|
|
318
|
+
// under a distinct name so it can be read for diagnostics but can never
|
|
319
|
+
// be mistaken for a fresh outcome.
|
|
320
|
+
...(receipt ? { replayedReceipt: receipt } : {}),
|
|
289
321
|
retryAfterMs: numberValue(response?.retryAfterMs) ?? 30_000,
|
|
290
322
|
changedCounts: null,
|
|
291
323
|
auditComplete: false,
|
|
@@ -329,6 +361,9 @@ export function normalizeSchedulerPrimitiveResult(value, expected) {
|
|
|
329
361
|
}
|
|
330
362
|
return {
|
|
331
363
|
status: schedulerStatus,
|
|
364
|
+
// fix(112ao-2): the completion decision, carried explicitly instead of
|
|
365
|
+
// leaving each consumer to re-infer it from the status string.
|
|
366
|
+
terminal: schedulerEnvelopeIsTerminal(schedulerStatus),
|
|
332
367
|
receipt: response?.receipt ?? null,
|
|
333
368
|
retryAfterMs: numberValue(response?.retryAfterMs),
|
|
334
369
|
changedCounts,
|
|
@@ -1410,7 +1410,8 @@ function sanitizeSchedulerExpectedTargets(value, scope) {
|
|
|
1410
1410
|
branchActionTypes.length === 0 ||
|
|
1411
1411
|
!scope.campaignIds.includes(campaignId) ||
|
|
1412
1412
|
!scope.tableIds.includes(tableId) ||
|
|
1413
|
-
senderIds.some((senderId) => !scope.senderIds.includes(senderId))
|
|
1413
|
+
senderIds.some((senderId) => !scope.senderIds.includes(senderId)) ||
|
|
1414
|
+
senderIds.some((senderId) => !scope.laneSelected(senderId, refillLaneKey))) {
|
|
1414
1415
|
return null;
|
|
1415
1416
|
}
|
|
1416
1417
|
expectedTargets.push({
|
|
@@ -1423,6 +1424,28 @@ function sanitizeSchedulerExpectedTargets(value, scope) {
|
|
|
1423
1424
|
}
|
|
1424
1425
|
return expectedTargets;
|
|
1425
1426
|
}
|
|
1427
|
+
// fix(112ao-3): sweep ELIGIBILITY is server-authoritative. This sanitizer
|
|
1428
|
+
// validates PROVENANCE and SHAPE — identity, revisions, date format, scope
|
|
1429
|
+
// containment, side-effect class, receipt requirements — and no longer
|
|
1430
|
+
// re-derives which lanes qualify for a sweep.
|
|
1431
|
+
//
|
|
1432
|
+
// The old contract required the server's emitted sender/lane set to EQUAL a
|
|
1433
|
+
// set this function re-derived from senderPlans, so any disagreement in
|
|
1434
|
+
// EITHER direction discarded the entire sweep:
|
|
1435
|
+
// - client stricter than server (112ak-s: `remainingReadyOrProjectedGap === 0`)
|
|
1436
|
+
// silently vetoed valid server sweeps until 112ak-s2 relaxed the copy;
|
|
1437
|
+
// - client looser than server (112ao-3) does exactly the same damage, because
|
|
1438
|
+
// the server excludes lanes for facts senderPlans never carries — an
|
|
1439
|
+
// exhausted per-date capacity probe (`schedulerFillableSlots === 0`), no
|
|
1440
|
+
// `selectedDay` on the target date, a non-structured lane, or a campaign
|
|
1441
|
+
// with no ready-holding ACTIVE source.
|
|
1442
|
+
// Keeping two copies of one predicate in sync across a release boundary is the
|
|
1443
|
+
// defect; relaxing the copy was only ever a truce. The client cannot see the
|
|
1444
|
+
// inputs the decision needs, so it must not make the decision.
|
|
1445
|
+
//
|
|
1446
|
+
// Scope containment is retained and is NOT eligibility: a sweep may only name
|
|
1447
|
+
// senders and lanes belonging to this run's selected cohort, so a drifted or
|
|
1448
|
+
// forged packet still fails closed.
|
|
1426
1449
|
function sanitizeTargetSchedulerSweep(params) {
|
|
1427
1450
|
if (!Array.isArray(params.value) ||
|
|
1428
1451
|
!params.expectedWorkspaceId ||
|
|
@@ -1432,13 +1455,12 @@ function sanitizeTargetSchedulerSweep(params) {
|
|
|
1432
1455
|
!params.expectedStateRevision) {
|
|
1433
1456
|
return null;
|
|
1434
1457
|
}
|
|
1435
|
-
const
|
|
1436
|
-
const
|
|
1437
|
-
|
|
1438
|
-
].sort();
|
|
1439
|
-
if (expectedSenderIds.length === 0 || expectedActionTypes.length === 0) {
|
|
1458
|
+
const selectedSenderIds = new Set(params.selectedBySender.keys());
|
|
1459
|
+
const selectedActionTypes = new Set([...params.selectedBySender.values()].flatMap((lanes) => [...lanes]));
|
|
1460
|
+
if (selectedSenderIds.size === 0 || selectedActionTypes.size === 0) {
|
|
1440
1461
|
return null;
|
|
1441
1462
|
}
|
|
1463
|
+
const laneSelected = (senderId, actionType) => params.selectedBySender.get(senderId)?.has(actionType) === true;
|
|
1442
1464
|
for (const rawAction of params.value) {
|
|
1443
1465
|
if (!isRecord(rawAction) || rawAction.type !== "run_scheduler_sweep") {
|
|
1444
1466
|
continue;
|
|
@@ -1451,7 +1473,7 @@ function sanitizeTargetSchedulerSweep(params) {
|
|
|
1451
1473
|
const campaignIds = sortedUniqueStrings(input.campaignIds);
|
|
1452
1474
|
const tableIds = sortedUniqueStrings(input.tableIds);
|
|
1453
1475
|
const sourceScopes = sanitizeSchedulerSweepSourceScopes(input.sourceScopes);
|
|
1454
|
-
const expectedTargets = sanitizeSchedulerExpectedTargets(input.expectedTargets, { senderIds, campaignIds, tableIds });
|
|
1476
|
+
const expectedTargets = sanitizeSchedulerExpectedTargets(input.expectedTargets, { senderIds, campaignIds, tableIds, laneSelected });
|
|
1455
1477
|
const expectedTargetsHash = stringValue(input.expectedTargetsHash)?.trim();
|
|
1456
1478
|
const maxPlacements = numberValue(input.maxPlacements);
|
|
1457
1479
|
const receiptRequirements = isRecord(rawAction.receiptRequirements)
|
|
@@ -1474,10 +1496,18 @@ function sanitizeTargetSchedulerSweep(params) {
|
|
|
1474
1496
|
input.stateRevision !== params.expectedStateRevision ||
|
|
1475
1497
|
!actionKey ||
|
|
1476
1498
|
requestKey !== actionKey ||
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1499
|
+
// Provenance, not eligibility: the sweep may only name senders and lanes
|
|
1500
|
+
// this run selected. Containment (not equality) lets the server exclude
|
|
1501
|
+
// a lane on facts the client cannot see without losing the whole sweep.
|
|
1502
|
+
senderIds.length === 0 ||
|
|
1503
|
+
senderIds.some((senderId) => !selectedSenderIds.has(senderId)) ||
|
|
1504
|
+
actionTypes.length === 0 ||
|
|
1505
|
+
actionTypes.some((actionType) => !selectedActionTypes.has(actionType)) ||
|
|
1506
|
+
// The server sets a top-level actionType only for a single-lane sweep,
|
|
1507
|
+
// derived from the packet's own actionTypes — so this stays a check of
|
|
1508
|
+
// the packet's internal consistency, at exactly the prior strictness
|
|
1509
|
+
// (multi-lane sweeps carry no top-level actionType and are not asserted).
|
|
1510
|
+
(actionTypes.length === 1 && rawActionType !== actionTypes[0]) ||
|
|
1481
1511
|
campaignIds.length === 0 ||
|
|
1482
1512
|
tableIds.length === 0 ||
|
|
1483
1513
|
sourceScopes.length === 0 ||
|
|
@@ -1876,41 +1906,20 @@ export function sanitizeRefillTargetPlanResult(result) {
|
|
|
1876
1906
|
const paidRefreshNeededSenderIdSet = paidRefreshNeededSenderIds(senderRefillPlans);
|
|
1877
1907
|
const blockers = sanitizeBlockers(result.blockers, selectedKeys, paidRefreshNeededSenderIdSet);
|
|
1878
1908
|
const request = isRecord(result.request) ? result.request : {};
|
|
1879
|
-
// fix(
|
|
1880
|
-
//
|
|
1881
|
-
//
|
|
1882
|
-
//
|
|
1883
|
-
//
|
|
1884
|
-
//
|
|
1885
|
-
//
|
|
1886
|
-
//
|
|
1887
|
-
//
|
|
1888
|
-
//
|
|
1889
|
-
//
|
|
1890
|
-
// sweep by HOLDING ready rows against an open dated slot, not by its ready
|
|
1891
|
-
// buffer fully covering its gap. No target-date condition is needed to mirror
|
|
1892
|
-
// the server's scoping — sanitizeTargetSchedulerSweep already refuses to run
|
|
1893
|
-
// without an expectedTargetDate matching ^\d{4}-\d{2}-\d{2}$, so this path is
|
|
1894
|
-
// explicit-date-only by construction. Deliberately the ONLY relaxed occurrence:
|
|
1895
|
-
// the wait_for_scheduler append and the status derivation below both keep
|
|
1896
|
-
// `remainingReadyOrProjectedGap === 0`, where full ready coverage is the
|
|
1897
|
-
// correct and intended meaning.
|
|
1898
|
-
const sweepEligibleBySender = new Map();
|
|
1899
|
-
for (const plan of senderPlans) {
|
|
1900
|
-
const senderId = stringValue(plan.senderId);
|
|
1901
|
-
const actionType = allowedActionType(plan.actionType);
|
|
1902
|
-
if (senderId &&
|
|
1903
|
-
actionType &&
|
|
1904
|
-
numberValue(plan.remainingProjectedGap) > 0 &&
|
|
1905
|
-
numberValue(plan.readyBuffer) > 0) {
|
|
1906
|
-
const lanes = sweepEligibleBySender.get(senderId) ?? new Set();
|
|
1907
|
-
lanes.add(actionType);
|
|
1908
|
-
sweepEligibleBySender.set(senderId, lanes);
|
|
1909
|
-
}
|
|
1910
|
-
}
|
|
1909
|
+
// fix(112ao-3): the client no longer re-derives sweep eligibility. It passes
|
|
1910
|
+
// the run's SELECTED cohort so sanitizeTargetSchedulerSweep can check scope
|
|
1911
|
+
// containment (provenance), and leaves the qualify/disqualify decision to
|
|
1912
|
+
// buildExactDateSchedulerSweepAction, which alone holds the per-date capacity
|
|
1913
|
+
// probe, selectedDays, and ready-supply campaign facts the decision needs.
|
|
1914
|
+
// 112ak-s2 relaxed a duplicated `remainingReadyOrProjectedGap === 0` copy
|
|
1915
|
+
// here after it vetoed valid Dotwork sweeps; the duplication itself was the
|
|
1916
|
+
// defect, since a client that is merely LOOSER than the server discards the
|
|
1917
|
+
// sweep just as completely. The two other occurrences below and above are
|
|
1918
|
+
// untouched and mean something different: the wait_for_scheduler append and
|
|
1919
|
+
// the status derivation both use full ready coverage correctly.
|
|
1911
1920
|
const schedulerSweepAction = sanitizeTargetSchedulerSweep({
|
|
1912
1921
|
value: target.globalActionQueue,
|
|
1913
|
-
|
|
1922
|
+
selectedBySender,
|
|
1914
1923
|
expectedWorkspaceId: stringValue(target.workspaceId),
|
|
1915
1924
|
expectedTargetDate: stringValue(request.targetDate),
|
|
1916
1925
|
expectedTargetShapeRevision: stringValue(result.targetShapeRevision),
|
package/package.json
CHANGED
|
@@ -33,7 +33,9 @@ You are a pipeline supply agent. People who engage with a sender's LinkedIn post
|
|
|
33
33
|
<inputs>
|
|
34
34
|
The invoking prompt names the senders ("refresh sender engagement for csreyes92 and thomas"). Resolve each via `list_senders` (match name/handle/LinkedIn URL). With no names given, inspect the active workspace and refresh every connected sender that has an active/paused sender-owned Post Engagers campaign backed by Signal Discovery.
|
|
35
35
|
|
|
36
|
-
Optional: target sender names/ids, `campaignId` when the user wants to force a specific Post Engagers campaign, `tableId` when the user wants to force a specific campaign table,
|
|
36
|
+
Optional: target sender names/ids, `campaignId` when the user wants to force a specific Post Engagers campaign, `tableId` when the user wants to force a specific campaign table, `maxPosts` to refresh a specific number of posts, and maximum engager pages per tracked post.
|
|
37
|
+
|
|
38
|
+
Leave `maxPosts` unset unless the user asked for a specific number. By default the command refreshes every tracked post published inside the 30-day lookback window, which is what keeps recent posts checked often enough to catch new engagers. A post whose engagement counts have not moved costs a single cheap count probe and is skipped before any engager fetch, so a wider sweep is not a proportionally more expensive one. Passing `maxPosts` narrows that sweep.
|
|
37
39
|
|
|
38
40
|
`campaignId` is optional. When it is omitted the command discovers the sender's Post Engagers campaign itself — it searches the workspace for campaigns the sender solely owns that are backed by a post-engager provider and named as a Post Engagers lane. A workspace can hold one such lane per sender, so discovery is always scoped to the sender you are refreshing; another sender's lane and shared multi-sender lanes are never selected. Do not pass a `campaignId` you guessed.
|
|
39
41
|
</inputs>
|
|
@@ -84,7 +86,7 @@ For each target sender/campaign:
|
|
|
84
86
|
- LinkedIn operations here are read-only fetches plus a typed product import into a campaign table. **No messages are generated, approved, scheduled, or sent by this skill.**
|
|
85
87
|
- Respect workspace boundaries: only add leads to campaigns in the active workspace, and only for senders that belong to it.
|
|
86
88
|
- Respect campaign boundaries: only add engagers to the matched sender-owned Post Engagers campaign/table. Do not mix shared-lane engagers into sender-owned campaigns or sender-owned engagers into shared lanes.
|
|
87
|
-
-
|
|
89
|
+
- Provider usage per run is bounded by the 30-day lookback window plus the count-probe short-circuit: unchanged posts cost one probe and fetch no engagers. Do not widen `maxEngagerPages` beyond what the user asked for.
|
|
88
90
|
- Never call `start_campaign`, `attach_sequence`, `queue_campaign_cells`, `start_campaign_message_preparation`, approval tools, send/schedule tools, or shared-lane mutation tools.
|
|
89
91
|
- Never create campaigns, change campaign status, change sender ownership, approve campaign cells, or start message prep as part of this refresh.
|
|
90
92
|
</safety>
|