@sellable/mcp 0.1.794 → 0.1.795

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.
@@ -493,6 +493,8 @@ type WaitInput = SchemaInput & {
493
493
  minPassedCount?: number;
494
494
  minGeneratedMessages?: number;
495
495
  templateRevision?: "current" | string;
496
+ /** Wait until the exact campaign table has no queued or processing cells. */
497
+ requireIdle?: boolean;
496
498
  timeoutMs?: number;
497
499
  intervalMs?: number;
498
500
  };
@@ -577,6 +579,7 @@ export declare const campaignProcessingToolDefinitions: ({
577
579
  minPassedCount?: undefined;
578
580
  minGeneratedMessages?: undefined;
579
581
  templateRevision?: undefined;
582
+ requireIdle?: undefined;
580
583
  timeoutMs?: undefined;
581
584
  intervalMs?: undefined;
582
585
  rowIds?: undefined;
@@ -654,6 +657,7 @@ export declare const campaignProcessingToolDefinitions: ({
654
657
  minPassedCount?: undefined;
655
658
  minGeneratedMessages?: undefined;
656
659
  templateRevision?: undefined;
660
+ requireIdle?: undefined;
657
661
  timeoutMs?: undefined;
658
662
  intervalMs?: undefined;
659
663
  rowIds?: undefined;
@@ -721,6 +725,7 @@ export declare const campaignProcessingToolDefinitions: ({
721
725
  minPassedCount?: undefined;
722
726
  minGeneratedMessages?: undefined;
723
727
  templateRevision?: undefined;
728
+ requireIdle?: undefined;
724
729
  timeoutMs?: undefined;
725
730
  intervalMs?: undefined;
726
731
  rowIds?: undefined;
@@ -757,6 +762,9 @@ export declare const campaignProcessingToolDefinitions: ({
757
762
  type: string;
758
763
  description: string;
759
764
  };
765
+ requireIdle: {
766
+ type: string;
767
+ };
760
768
  timeoutMs: {
761
769
  type: string;
762
770
  };
@@ -814,6 +822,7 @@ export declare const campaignProcessingToolDefinitions: ({
814
822
  minPassedCount?: undefined;
815
823
  minGeneratedMessages?: undefined;
816
824
  templateRevision?: undefined;
825
+ requireIdle?: undefined;
817
826
  timeoutMs?: undefined;
818
827
  intervalMs?: undefined;
819
828
  targetConfig?: undefined;
@@ -1035,6 +1044,7 @@ export declare const campaignProcessingToolDefinitions: ({
1035
1044
  minPassedCount?: undefined;
1036
1045
  minGeneratedMessages?: undefined;
1037
1046
  templateRevision?: undefined;
1047
+ requireIdle?: undefined;
1038
1048
  timeoutMs?: undefined;
1039
1049
  intervalMs?: undefined;
1040
1050
  rowIds?: undefined;
@@ -1067,6 +1077,7 @@ export declare function waitForCampaignProcessing(input: WaitInput): Promise<{
1067
1077
  currentTemplateRevisionHash: string;
1068
1078
  guidance: string;
1069
1079
  stats: WorkflowTableStats;
1080
+ idle?: undefined;
1070
1081
  floors?: undefined;
1071
1082
  result?: undefined;
1072
1083
  warning?: undefined;
@@ -1078,6 +1089,7 @@ export declare function waitForCampaignProcessing(input: WaitInput): Promise<{
1078
1089
  elapsedMs: number;
1079
1090
  tableId: string;
1080
1091
  pollKey: string;
1092
+ idle: boolean;
1081
1093
  floors: {
1082
1094
  minPassedCount: number | null;
1083
1095
  minGeneratedMessages: number | null;
@@ -1090,6 +1102,8 @@ export declare function waitForCampaignProcessing(input: WaitInput): Promise<{
1090
1102
  staleGeneratedMessagesCount: number;
1091
1103
  generatedWithoutPassCount: number;
1092
1104
  currentTemplateRevisionHash: string | null;
1105
+ queuedCount: number | null;
1106
+ processingCount: number | null;
1093
1107
  };
1094
1108
  stats: WorkflowTableStats;
1095
1109
  reason?: undefined;
@@ -1116,6 +1130,9 @@ export declare function waitForCampaignProcessing(input: WaitInput): Promise<{
1116
1130
  currentTemplateRevisionHash: string | null;
1117
1131
  passFloorMet: boolean;
1118
1132
  generatedFloorMet: boolean;
1133
+ idle: boolean;
1134
+ queuedCount: number | null;
1135
+ processingCount: number | null;
1119
1136
  };
1120
1137
  recovery: {
1121
1138
  checkpoint: string;
@@ -1146,6 +1163,7 @@ export declare function waitForCampaignProcessing(input: WaitInput): Promise<{
1146
1163
  stats: WorkflowTableStats | null;
1147
1164
  requestedTemplateRevision?: undefined;
1148
1165
  currentTemplateRevisionHash?: undefined;
1166
+ idle?: undefined;
1149
1167
  floors?: undefined;
1150
1168
  result?: undefined;
1151
1169
  }>;
@@ -434,7 +434,7 @@ export const campaignProcessingToolDefinitions = [
434
434
  },
435
435
  {
436
436
  name: "wait_for_campaign_processing",
437
- description: "Stats-only campaign processing wait. Use for fit/message readiness instead of wait_for_rubric_results when waiting on campaign-table processing. Generated messages are counted separately from rubric passes; current template revision is enforced through messageDraftBasis.approvedBriefHash.",
437
+ description: "Stats-only campaign processing wait. Use for fit/message readiness or requireIdle:true when an exact campaign table must settle before replanning. Generated messages are counted separately from rubric passes; current template revision is enforced through messageDraftBasis.approvedBriefHash.",
438
438
  inputSchema: {
439
439
  type: "object",
440
440
  properties: {
@@ -447,6 +447,7 @@ export const campaignProcessingToolDefinitions = [
447
447
  type: "string",
448
448
  description: '"current" (default when minGeneratedMessages is set) or an explicit current approved brief hash.',
449
449
  },
450
+ requireIdle: { type: "boolean" },
450
451
  timeoutMs: { type: "number" },
451
452
  intervalMs: { type: "number" },
452
453
  },
@@ -603,6 +604,7 @@ function buildProcessingTimeoutRecovery({ input, schema, stats, minPassedCount,
603
604
  ...(minPassedCount ? { minPassedCount } : {}),
604
605
  ...(minGeneratedMessages ? { minGeneratedMessages } : {}),
605
606
  ...(templateRevision ? { templateRevision } : {}),
607
+ ...(input.requireIdle ? { requireIdle: true } : {}),
606
608
  },
607
609
  });
608
610
  }
@@ -693,11 +695,13 @@ export async function waitForCampaignProcessing(input) {
693
695
  : null;
694
696
  const templateRevision = input.templateRevision ??
695
697
  (minGeneratedMessages !== null ? "current" : null);
698
+ const requireIdle = input.requireIdle === true;
696
699
  const pollKey = stableHash({
697
700
  tableId: schema.tableId,
698
701
  minPassedCount,
699
702
  minGeneratedMessages,
700
703
  templateRevision,
704
+ requireIdle,
701
705
  });
702
706
  const start = Date.now();
703
707
  let attempts = 0;
@@ -732,13 +736,19 @@ export async function waitForCampaignProcessing(input) {
732
736
  : (stats.generatedMessagesCount ?? stats.messagesCount ?? 0);
733
737
  const passFloorMet = minPassedCount === null || passed >= minPassedCount;
734
738
  const generatedFloorMet = minGeneratedMessages === null || generated >= minGeneratedMessages;
735
- if (passFloorMet && generatedFloorMet) {
739
+ const queuedCount = typeof stats.queuedCount === "number" ? stats.queuedCount : null;
740
+ const processingCount = typeof stats.processingCount === "number" ? stats.processingCount : null;
741
+ const idle = queuedCount !== null &&
742
+ processingCount !== null &&
743
+ queuedCount + processingCount === 0;
744
+ if (passFloorMet && generatedFloorMet && (!requireIdle || idle)) {
736
745
  return {
737
746
  ready: true,
738
747
  attempts,
739
748
  elapsedMs: Date.now() - start,
740
749
  tableId: schema.tableId,
741
750
  pollKey,
751
+ idle,
742
752
  floors: {
743
753
  minPassedCount,
744
754
  minGeneratedMessages,
@@ -751,6 +761,8 @@ export async function waitForCampaignProcessing(input) {
751
761
  staleGeneratedMessagesCount: stats.staleGeneratedMessagesCount ?? 0,
752
762
  generatedWithoutPassCount: stats.generatedWithoutPassCount ?? 0,
753
763
  currentTemplateRevisionHash: currentRevisionHash,
764
+ queuedCount,
765
+ processingCount,
754
766
  },
755
767
  stats,
756
768
  };
@@ -788,6 +800,11 @@ export async function waitForCampaignProcessing(input) {
788
800
  generatedFloorMet: minGeneratedMessages === null ||
789
801
  (lastStats?.currentRevisionGeneratedMessagesCount ?? 0) >=
790
802
  minGeneratedMessages,
803
+ idle: typeof lastStats?.queuedCount === "number" &&
804
+ typeof lastStats?.processingCount === "number" &&
805
+ lastStats.queuedCount + lastStats.processingCount === 0,
806
+ queuedCount: lastStats?.queuedCount ?? null,
807
+ processingCount: lastStats?.processingCount ?? null,
791
808
  },
792
809
  recovery,
793
810
  guidance: "Surface this partial validation checkpoint before taking another action. Use recovery.suggestedToolCalls to retry processing, queue missing work, revise rubrics, record a fresh same-source sample, revise source, or revise message quality instead of moving to Settings on a weak sample.",
@@ -86,10 +86,10 @@ type IntegrationHomeWorkspacePin = {
86
86
  };
87
87
  /**
88
88
  * Mirror the home-workspace consistency invariant owned by
89
- * `readMcpAgentServiceContext` without importing that module into this hot tool
90
- * surface. `auth.ts` and `agent-service-auth.ts` remain the credential authority;
91
- * this local read exists only to put their already-validated HOME value into the
92
- * request option bag.
89
+ * `readMcpAgentServiceContext`. Customer profiles prove home with the customer
90
+ * workspace lock. Cross-workspace Admin profiles intentionally omit that lock,
91
+ * so their complete signed Agent service context plus the server-authenticated
92
+ * Admin kind proves home instead.
93
93
  *
94
94
  * The server independently enforces the same boundary. This client pin makes the
95
95
  * tool predictable while the active workspace is elsewhere; it is not the
@@ -1,3 +1,4 @@
1
+ import { readMcpAgentServiceContext } from "../agent-service-auth.js";
1
2
  import { getApi, SellableApiError } from "../api.js";
2
3
  import { getConfig } from "../auth.js";
3
4
  /**
@@ -93,10 +94,10 @@ const READ_TIMEOUT_MS = 25_000;
93
94
  const CALL_TIMEOUT_MS = 45_000;
94
95
  /**
95
96
  * Mirror the home-workspace consistency invariant owned by
96
- * `readMcpAgentServiceContext` without importing that module into this hot tool
97
- * surface. `auth.ts` and `agent-service-auth.ts` remain the credential authority;
98
- * this local read exists only to put their already-validated HOME value into the
99
- * request option bag.
97
+ * `readMcpAgentServiceContext`. Customer profiles prove home with the customer
98
+ * workspace lock. Cross-workspace Admin profiles intentionally omit that lock,
99
+ * so their complete signed Agent service context plus the server-authenticated
100
+ * Admin kind proves home instead.
100
101
  *
101
102
  * The server independently enforces the same boundary. This client pin makes the
102
103
  * tool predictable while the active workspace is elsewhere; it is not the
@@ -109,7 +110,27 @@ export function resolveIntegrationHomeWorkspacePin(env = process.env, token = ge
109
110
  const workspaceId = env.SELLABLE_AGENT_WORKSPACE_ID?.trim();
110
111
  const lockWorkspaceId = env.SELLABLE_LOCK_WORKSPACE_ID?.trim();
111
112
  const requiresLock = ["1", "true", "yes"].includes((env.SELLABLE_REQUIRE_WORKSPACE_LOCK ?? "").trim().toLowerCase());
112
- if (!workspaceId || workspaceId !== lockWorkspaceId || !requiresLock) {
113
+ let serviceContextWorkspaceId = null;
114
+ try {
115
+ serviceContextWorkspaceId =
116
+ readMcpAgentServiceContext(env, token)?.workspaceId ?? null;
117
+ }
118
+ catch {
119
+ serviceContextWorkspaceId = null;
120
+ }
121
+ const authenticatedKind = (env.SELLABLE_AGENT_SERVER_AUTHENTICATED_KIND ?? "")
122
+ .trim()
123
+ .toUpperCase();
124
+ const verifiedCustomerHome = Boolean(workspaceId) &&
125
+ workspaceId === serviceContextWorkspaceId &&
126
+ workspaceId === lockWorkspaceId &&
127
+ requiresLock;
128
+ const verifiedAdminHome = authenticatedKind === "ADMIN" &&
129
+ Boolean(workspaceId) &&
130
+ workspaceId === serviceContextWorkspaceId &&
131
+ !lockWorkspaceId &&
132
+ !requiresLock;
133
+ if (!verifiedCustomerHome && !verifiedAdminHome) {
113
134
  return {
114
135
  ok: false,
115
136
  envelope: envelope({
@@ -652,6 +652,7 @@ export declare const allTools: ({
652
652
  minPassedCount?: undefined;
653
653
  minGeneratedMessages?: undefined;
654
654
  templateRevision?: undefined;
655
+ requireIdle?: undefined;
655
656
  timeoutMs?: undefined;
656
657
  intervalMs?: undefined;
657
658
  rowIds?: undefined;
@@ -729,6 +730,7 @@ export declare const allTools: ({
729
730
  minPassedCount?: undefined;
730
731
  minGeneratedMessages?: undefined;
731
732
  templateRevision?: undefined;
733
+ requireIdle?: undefined;
732
734
  timeoutMs?: undefined;
733
735
  intervalMs?: undefined;
734
736
  rowIds?: undefined;
@@ -796,6 +798,7 @@ export declare const allTools: ({
796
798
  minPassedCount?: undefined;
797
799
  minGeneratedMessages?: undefined;
798
800
  templateRevision?: undefined;
801
+ requireIdle?: undefined;
799
802
  timeoutMs?: undefined;
800
803
  intervalMs?: undefined;
801
804
  rowIds?: undefined;
@@ -832,6 +835,9 @@ export declare const allTools: ({
832
835
  type: string;
833
836
  description: string;
834
837
  };
838
+ requireIdle: {
839
+ type: string;
840
+ };
835
841
  timeoutMs: {
836
842
  type: string;
837
843
  };
@@ -889,6 +895,7 @@ export declare const allTools: ({
889
895
  minPassedCount?: undefined;
890
896
  minGeneratedMessages?: undefined;
891
897
  templateRevision?: undefined;
898
+ requireIdle?: undefined;
892
899
  timeoutMs?: undefined;
893
900
  intervalMs?: undefined;
894
901
  targetConfig?: undefined;
@@ -1110,6 +1117,7 @@ export declare const allTools: ({
1110
1117
  minPassedCount?: undefined;
1111
1118
  minGeneratedMessages?: undefined;
1112
1119
  templateRevision?: undefined;
1120
+ requireIdle?: undefined;
1113
1121
  timeoutMs?: undefined;
1114
1122
  intervalMs?: undefined;
1115
1123
  rowIds?: undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.794",
3
+ "version": "0.1.795",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code, Codex, and Hermes campaign workflows",
6
6
  "main": "dist/index.js",
@@ -18,6 +18,7 @@ allowed-tools:
18
18
  - mcp__sellable__search_subskill_prompts
19
19
  - mcp__sellable__list_senders
20
20
  - mcp__sellable__select_campaign_cells
21
+ - mcp__sellable__wait_for_campaign_processing
21
22
  ---
22
23
 
23
24
  # Refill Sends
@@ -591,8 +592,16 @@ asset is the contract to load — load the prompt and that asset, verify the `v3
591
592
  compatible range, and report the version before the first call. Call
592
593
  `refill_v3_advance` with `{workspaceId, scope}` — plus `yolo: true` only when the
593
594
  operator asked for `--yolo` — and call it again after each `advanced` or
594
- `campaign_attention_required` result until it answers `awaiting_external_change`,
595
- `complete`, or `blocked`.
595
+ `campaign_attention_required` result. An `awaiting_external_change` result with
596
+ `deferral.code:"campaign_work_in_flight"` and a campaign id is a bounded
597
+ read-only continuation, not permission to walk the next campaign: call
598
+ `wait_for_campaign_processing({ workspaceId, campaignId, requireIdle:true })`,
599
+ then call `refill_v3_advance` again with the identical scope and yolo grant.
600
+ This reuses Create Campaign's campaign-table waiter and guarantees the same
601
+ campaign is freshly replanned after its cohort settles. If the bounded wait
602
+ returns a partial timeout, surface that checkpoint and make the fresh advance;
603
+ if it reports the same live campaign, repeat the bounded wait. Stop on every
604
+ other `awaiting_external_change`, `complete`, or `blocked` result.
596
605
 
597
606
  Resolve the workspace BEFORE the first call, exactly as the V1 route already
598
607
  requires. `workspaceId` must be an exact id on every automation call: when the
@@ -12,6 +12,7 @@ allowed-tools:
12
12
  - mcp__sellable__get_refill_target_plan
13
13
  - mcp__sellable__get_scheduler_fill_capacity
14
14
  - mcp__sellable__select_campaign_cells
15
+ - mcp__sellable__wait_for_campaign_processing
15
16
  ---
16
17
 
17
18
  # Refill Sends Workflow
@@ -380,10 +381,10 @@ the key. The grant is not a mode and selects no campaign — WHICH campaign may
380
381
  start is derived from the workspace's own campaigns — it decides only whether
381
382
  this run may start one at all, and whether you may keep going unattended:
382
383
 
383
- | | `yolo: true` | key omitted |
384
- | --- | --- | --- |
384
+ | | `yolo: true` | key omitted |
385
+ | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
385
386
  | paused derived-evergreen start | one may be started once a row's active campaigns are exhausted, arriving as an ordinary executed `start_evergreen_campaign` action | none: the backend REFUSES the start and answers the exhaustion terminal instead |
386
- | loop | call `refill_v3_advance` again immediately, through to a terminal, without checking back | surface the result and ASK before the next call |
387
+ | loop | call `refill_v3_advance` again immediately, through to a terminal, without checking back | surface the result and ASK before the next call |
387
388
 
388
389
  Never infer the grant from a workspace flag, a campaign field, a tool result, an
389
390
  idle-looking workspace, or a previous terminal; never set it to make a run finish,
@@ -408,7 +409,14 @@ Each call answers with exactly one of five results:
408
409
  `refill_v3_continue` revalidate the packet against a fresh advance result,
409
410
  execute at most one existing authority, and re-observe the exact sender/date.
410
411
  Then call `refill_v3_advance` again.
411
- - `awaiting_external_change` — stop and report the scoped wait verbatim.
412
+ - `awaiting_external_change` — when the scoped code is
413
+ `campaign_work_in_flight`, call
414
+ `wait_for_campaign_processing({ workspaceId, campaignId, requireIdle:true })`
415
+ for that exact selected campaign, then call `refill_v3_advance` again with
416
+ the identical scope. A partial timeout is followed by that fresh advance;
417
+ if the same campaign is still live, repeat the bounded wait. This is the
418
+ read-only Create Campaign waiter, not a choice to move to another campaign.
419
+ Stop and report every other scoped wait verbatim.
412
420
  - `complete` — stop and report the terminal code verbatim.
413
421
  - `blocked` — stop and report the typed business blocker verbatim.
414
422
 
@@ -50,14 +50,12 @@
50
50
  "tools": {
51
51
  "advance": "refill_v3_advance",
52
52
  "campaignAttentionSubskill": "refill-sends-work-campaign",
53
- "campaignAttentionAuthority": "refill_v3_continue"
53
+ "campaignAttentionAuthority": "refill_v3_continue",
54
+ "campaignProcessingWait": "wait_for_campaign_processing"
54
55
  },
55
56
  "workspaceResolution": {
56
57
  "appliesWhen": "always, BEFORE the first advance call",
57
- "allowedTools": [
58
- "list_workspaces",
59
- "get_active_workspace"
60
- ],
58
+ "allowedTools": ["list_workspaces", "get_active_workspace"],
61
59
  "rules": [
62
60
  "An automation call requires an exact explicit workspaceId. Never pass a workspace NAME to refill_v3_advance and never let it fall back to the configured workspace when the operator named one.",
63
61
  "When the operator names a workspace, call list_workspaces FIRST and match that name to an exact workspaceId; pass that id on every advance call for the rest of the run.",
@@ -98,26 +96,16 @@
98
96
  "loop": {
99
97
  "call": {
100
98
  "tool": "refill_v3_advance",
101
- "inputKeys": [
102
- "workspaceId",
103
- "scope",
104
- "yolo"
105
- ],
99
+ "inputKeys": ["workspaceId", "scope", "yolo"],
106
100
  "note": "Pass workspaceId and scope with the values the host resolved once, plus yolo:true only when the autonomy step granted it. workspaceId is the EXACT id workspaceResolution produced, never a name and never an implicit config default when the operator named a workspace. There is no mode, continuation, task receipt, retry counter, cursor, row, action, or receipt-fact input, and a call that supplies one is refused by the backend parser before any observation.",
107
- "requiredInputKeys": [
108
- "workspaceId",
109
- "scope"
110
- ]
101
+ "requiredInputKeys": ["workspaceId", "scope"]
111
102
  },
112
103
  "repeatWhile": [
113
104
  "advanced",
114
- "campaign_attention_required"
115
- ],
116
- "stopOn": [
117
- "awaiting_external_change",
118
- "complete",
119
- "blocked"
105
+ "campaign_attention_required",
106
+ "awaiting_external_change:campaign_work_in_flight_after_wait"
120
107
  ],
108
+ "stopOn": ["awaiting_external_change:other", "complete", "blocked"],
121
109
  "note": "Call advance again with the SAME two keys. Losing a prior result is harmless: every invocation reconstructs from fresh world state plus server-loaded authority receipts.",
122
110
  "repeatPolicy": {
123
111
  "granted": "Call advance again immediately, without checking back.",
@@ -143,18 +131,9 @@
143
131
  "alreadyExecuted": false,
144
132
  "route": {
145
133
  "subskill": "refill-sends-work-campaign",
146
- "pass": [
147
- "packet.scope",
148
- "packet.evidence"
149
- ],
134
+ "pass": ["packet.scope", "packet.evidence"],
150
135
  "returnThrough": "refill_v3_continue",
151
- "returnInput": [
152
- "workspaceId",
153
- "scope",
154
- "packet",
155
- "decision",
156
- "yolo"
157
- ]
136
+ "returnInput": ["workspaceId", "scope", "packet", "decision", "yolo"]
158
137
  },
159
138
  "then": "call the advance tool again",
160
139
  "note": "This is the ONE model judgment in the flow. The backend already selected exactly one campaign and bounded the evidence; the subskill chooses one action from its own closed vocabulary and its deterministic authority validates, executes, and re-observes it."
@@ -168,14 +147,24 @@
168
147
  "deferral.code",
169
148
  "deferral.resumeAt"
170
149
  ],
171
- "then": "stop and report the scoped wait verbatim"
150
+ "campaignWorkInFlight": {
151
+ "appliesToDeferralCode": "campaign_work_in_flight",
152
+ "wait": {
153
+ "tool": "wait_for_campaign_processing",
154
+ "input": {
155
+ "campaignId": "deferral.campaignId",
156
+ "workspaceId": "workspaceId",
157
+ "requireIdle": true
158
+ }
159
+ },
160
+ "then": "call the advance tool again with the identical workspaceId, scope, and yolo grant",
161
+ "note": "This is a bounded read-only wait inherited from Create Campaign. It holds the same campaign while its cells settle; it never authorizes selecting a sibling. A partial timeout returns to fresh advance, and the same deferral may authorize another bounded wait."
162
+ },
163
+ "then": "for any other deferral code, stop and report the scoped wait verbatim"
172
164
  },
173
165
  "complete": {
174
166
  "alreadyExecuted": false,
175
- "surface": [
176
- "terminal",
177
- "observation"
178
- ],
167
+ "surface": ["terminal", "observation"],
179
168
  "then": "stop and report the terminal code verbatim"
180
169
  },
181
170
  "blocked": {
@@ -204,6 +193,6 @@
204
193
  "Every advance call carried exactly workspaceId and scope.",
205
194
  "Every already-executed result was surfaced, never re-executed.",
206
195
  "Only campaign_attention_required loaded the campaign subskill.",
207
- "The run stopped on the first awaiting_external_change, complete, or blocked result."
196
+ "campaign_work_in_flight used the bounded campaign table waiter and returned to a fresh advance before any sibling campaign; every other awaiting_external_change, complete, or blocked result stopped the run."
208
197
  ]
209
198
  }