@sellable/install 0.1.362-phase111.20260720123904 → 0.1.362
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/bin/sellable-agent-host-bootstrap.mjs +26 -0
- package/bin/sellable-install.mjs +7 -2
- package/container/Dockerfile +41 -0
- package/container/README.md +15 -0
- package/container/compose.yaml +22 -0
- package/container/entrypoint.sh +17 -0
- package/lib/sellable-agent/external-runtime-builder.mjs +272 -0
- package/lib/sellable-agent/hermes-bridge.mjs +4 -4
- package/lib/sellable-agent/host-bootstrap.mjs +458 -0
- package/lib/sellable-agent/host-worker.mjs +16 -20
- package/lib/sellable-agent/profile-materializer.mjs +2 -2
- package/lib/sellable-agent/provisioning-adapter.mjs +5 -5
- package/lib/sellable-agent/runtime-helper.mjs +107 -10
- package/lib/sellable-agent/service-installer.mjs +2 -0
- package/package.json +3 -1
- package/skill-templates/refill-sends-evergreen.md +0 -5
- package/skill-templates/refill-sends-v2.md +1 -36
- package/skill-templates/refill-sends.md +1 -33
|
@@ -23,6 +23,7 @@ export const EXTERNAL_HERMES_ENTRYPOINT = `${EXTERNAL_RUNTIME_CLOSURE_ROOT}/herm
|
|
|
23
23
|
const EXTERNAL_RUNTIME_MANIFEST_VERSION = "sellable-agent-external-runtime-closure/v1";
|
|
24
24
|
const ACTIONS = new Set([
|
|
25
25
|
"INSTALL_MCP_CREDENTIAL",
|
|
26
|
+
"INSTALL_PROVIDER_CREDENTIAL",
|
|
26
27
|
"INSTALL_RUNTIME_SERVICE_ENV",
|
|
27
28
|
"SET_RUNTIME_GATES",
|
|
28
29
|
"OBSERVE_RUNTIME_GATES",
|
|
@@ -50,6 +51,7 @@ const PROFILE_ENTRIES = Object.freeze([
|
|
|
50
51
|
]);
|
|
51
52
|
const ACTION_KEYS = Object.freeze({
|
|
52
53
|
INSTALL_MCP_CREDENTIAL: ["action", "profileId", "operationId", "fence", "sourceRequestFile", "generation", "fingerprint"],
|
|
54
|
+
INSTALL_PROVIDER_CREDENTIAL: ["action", "profileId", "operationId", "fence", "fingerprint"],
|
|
53
55
|
INSTALL_RUNTIME_SERVICE_ENV: ["action", "profileId", "operationId", "fence", "revisionId", "profileDigest"],
|
|
54
56
|
SET_RUNTIME_GATES: ["action", "profileId", "operationId", "fence", "lifecycle"],
|
|
55
57
|
OBSERVE_RUNTIME_GATES: ["action", "profileId", "operationId", "fence", "lifecycle"],
|
|
@@ -109,6 +111,79 @@ function atomicCredential(pathValue, secret, uid, gid) {
|
|
|
109
111
|
fsyncDirectory(dirname(pathValue));
|
|
110
112
|
}
|
|
111
113
|
|
|
114
|
+
function providerCredentialPath(profile, profileId) {
|
|
115
|
+
const runtimeStateRoot = safeRuntimeStateRoot(profile);
|
|
116
|
+
const profilesRoot = join(runtimeStateRoot, "profiles");
|
|
117
|
+
const profileRoot = join(profilesRoot, profileId);
|
|
118
|
+
const runtimeHome = join(profileRoot, "home");
|
|
119
|
+
for (const pathValue of [profilesRoot, profileRoot, runtimeHome]) {
|
|
120
|
+
const link = lstatSync(pathValue);
|
|
121
|
+
if (
|
|
122
|
+
link.isSymbolicLink() || !link.isDirectory() || (link.mode & 0o777) !== 0o700 ||
|
|
123
|
+
link.uid !== profile.runtimeUid || link.gid !== profile.runtimeGid
|
|
124
|
+
) throw new Error("provider runtime home rejected");
|
|
125
|
+
}
|
|
126
|
+
const codexRoot = join(runtimeHome, ".codex");
|
|
127
|
+
if (!existsSync(codexRoot)) {
|
|
128
|
+
mkdirSync(codexRoot, { mode: 0o700 });
|
|
129
|
+
chownSync(codexRoot, profile.runtimeUid, profile.runtimeGid);
|
|
130
|
+
}
|
|
131
|
+
const codex = lstatSync(codexRoot);
|
|
132
|
+
if (
|
|
133
|
+
codex.isSymbolicLink() || !codex.isDirectory() || (codex.mode & 0o777) !== 0o700 ||
|
|
134
|
+
codex.uid !== profile.runtimeUid || codex.gid !== profile.runtimeGid
|
|
135
|
+
) throw new Error("provider credential root rejected");
|
|
136
|
+
return join(codexRoot, "auth.json");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function installProviderCredential(profile, request, config) {
|
|
140
|
+
if (
|
|
141
|
+
!SHA256.test(request.fingerprint ?? "") ||
|
|
142
|
+
request.fingerprint !== profile.providerCredentialFingerprint ||
|
|
143
|
+
(
|
|
144
|
+
profile.providerCredentialSourcePath !== "/var/lib/sellable-agent-bootstrap/provider-auth" &&
|
|
145
|
+
config.disposableFixture !== true
|
|
146
|
+
)
|
|
147
|
+
) throw new Error("provider credential identity rejected");
|
|
148
|
+
const sourceRoot = privateRoot(dirname(profile.providerCredentialSourcePath));
|
|
149
|
+
const source = readPrivateDirectFile(sourceRoot, profile.providerCredentialSourcePath).trim();
|
|
150
|
+
if (!source || source.length > 1024 * 1024 || sha256(source) !== request.fingerprint) {
|
|
151
|
+
throw new Error("provider credential source rejected");
|
|
152
|
+
}
|
|
153
|
+
const parsed = JSON.parse(source);
|
|
154
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
155
|
+
throw new Error("provider credential JSON rejected");
|
|
156
|
+
}
|
|
157
|
+
const target = providerCredentialPath(profile, request.profileId);
|
|
158
|
+
atomicCredential(target, source, profile.runtimeUid, profile.runtimeGid);
|
|
159
|
+
return {
|
|
160
|
+
ok: true,
|
|
161
|
+
profileId: request.profileId,
|
|
162
|
+
fingerprint: request.fingerprint,
|
|
163
|
+
path: target,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function removeProviderCredential(profile, profileId) {
|
|
168
|
+
const target = providerCredentialPath(profile, profileId);
|
|
169
|
+
if (existsSync(target)) {
|
|
170
|
+
const link = lstatSync(target);
|
|
171
|
+
if (link.isSymbolicLink() || !link.isFile()) throw new Error("provider credential cleanup rejected");
|
|
172
|
+
rmSync(target, { force: true });
|
|
173
|
+
fsyncDirectory(dirname(target));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function observeProviderCredential(profile, profileId) {
|
|
178
|
+
const target = providerCredentialPath(profile, profileId);
|
|
179
|
+
const link = lstatSync(target);
|
|
180
|
+
if (
|
|
181
|
+
link.isSymbolicLink() || !link.isFile() || (link.mode & 0o777) !== 0o600 ||
|
|
182
|
+
link.uid !== profile.runtimeUid || link.gid !== profile.runtimeGid
|
|
183
|
+
) throw new Error("provider credential readback rejected");
|
|
184
|
+
return sha256(readFileSync(target, "utf8").trim());
|
|
185
|
+
}
|
|
186
|
+
|
|
112
187
|
function applyRuntimeOwnership(pathValue, uid, gid) {
|
|
113
188
|
const link = lstatSync(pathValue);
|
|
114
189
|
if (link.isSymbolicLink()) throw new Error("profile symlink rejected");
|
|
@@ -509,7 +584,9 @@ function walkExternalRoot(root, ownerUid, ownerGid) {
|
|
|
509
584
|
}
|
|
510
585
|
};
|
|
511
586
|
visit(root, "");
|
|
512
|
-
|
|
587
|
+
const byPath = (left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0;
|
|
588
|
+
const byLogicalPath = (left, right) => left.logicalPath < right.logicalPath ? -1 : left.logicalPath > right.logicalPath ? 1 : 0;
|
|
589
|
+
return { directories: directories.sort(byPath), files: files.sort(byLogicalPath) };
|
|
513
590
|
}
|
|
514
591
|
|
|
515
592
|
export function observeExternalRuntimeClosure(input = {}) {
|
|
@@ -569,18 +646,24 @@ export function observeExternalRuntimeClosure(input = {}) {
|
|
|
569
646
|
if (
|
|
570
647
|
JSON.stringify(rootRecord.directories.map((directory) => directory?.path)) !==
|
|
571
648
|
JSON.stringify(rootRecord.directories.map((directory) => directory?.path).sort()) ||
|
|
572
|
-
new Set(rootRecord.directories.map((directory) => directory?.path)).size !== rootRecord.directories.length
|
|
649
|
+
new Set(rootRecord.directories.map((directory) => directory?.path)).size !== rootRecord.directories.length
|
|
650
|
+
) throw new Error("external runtime directory order rejected");
|
|
651
|
+
if (
|
|
573
652
|
rootRecord.directories.some((directory) =>
|
|
574
653
|
!exactKeys(directory, ["path", "mode"]) ||
|
|
575
|
-
!/^[A-Za-z0-9
|
|
654
|
+
!/^[-A-Za-z0-9@._+]+(?:\/[-A-Za-z0-9@._+]+)*$/.test(directory.path ?? "") ||
|
|
576
655
|
directory.mode !== 0o555
|
|
577
|
-
)
|
|
578
|
-
|
|
656
|
+
)
|
|
657
|
+
) throw new Error("external runtime directory schema rejected");
|
|
658
|
+
if (JSON.stringify(walked.directories) !== JSON.stringify(rootRecord.directories)) {
|
|
659
|
+
throw new Error("external runtime directory inventory rejected");
|
|
660
|
+
}
|
|
661
|
+
if (
|
|
579
662
|
rootRecord.files.length === 0 ||
|
|
580
663
|
JSON.stringify(rootRecord.files.map((file) => file?.path)) !==
|
|
581
664
|
JSON.stringify(rootRecord.files.map((file) => file?.path).sort()) ||
|
|
582
665
|
new Set(rootRecord.files.map((file) => file?.path)).size !== rootRecord.files.length
|
|
583
|
-
) throw new Error("external runtime
|
|
666
|
+
) throw new Error("external runtime file order rejected");
|
|
584
667
|
const actualPaths = walked.files.map((file) => file.logicalPath);
|
|
585
668
|
if (JSON.stringify(actualPaths) !== JSON.stringify(rootRecord.files.map((file) => file.path))) {
|
|
586
669
|
throw new Error("external runtime open inventory rejected");
|
|
@@ -588,7 +671,7 @@ export function observeExternalRuntimeClosure(input = {}) {
|
|
|
588
671
|
const observedFiles = rootRecord.files.map((file, index) => {
|
|
589
672
|
if (
|
|
590
673
|
!exactKeys(file, ["path", "mode", "digest"]) ||
|
|
591
|
-
!/^[A-Za-z0-9
|
|
674
|
+
!/^[-A-Za-z0-9@._+]+(?:\/[-A-Za-z0-9@._+]+)*$/.test(file.path ?? "") ||
|
|
592
675
|
![0o444, 0o555].includes(file.mode) || !SHA256.test(file.digest ?? "")
|
|
593
676
|
) throw new Error("external runtime file schema rejected");
|
|
594
677
|
const actual = walked.files[index];
|
|
@@ -958,9 +1041,19 @@ async function observeRuntime(profile, request) {
|
|
|
958
1041
|
const profileFiles = observeRuntimeProfileFiles(target);
|
|
959
1042
|
const { runtimeConfig, runtimeMcp, tools, fileHashes: actualFileHashes } = profileFiles;
|
|
960
1043
|
const actualProfileDigest = profileFiles.profileDigest;
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1044
|
+
let observedProvider;
|
|
1045
|
+
try {
|
|
1046
|
+
observedProvider = observeProviderCredential(profile, request.profileId);
|
|
1047
|
+
if (observedProvider !== runtimeConfig?.provider?.fingerprint) {
|
|
1048
|
+
throw new Error("provider credential fingerprint rejected");
|
|
1049
|
+
}
|
|
1050
|
+
} catch (error) {
|
|
1051
|
+
if (!driftMode) throw error;
|
|
1052
|
+
observedProvider = sha256(`provider-drift\0${runtimeConfig?.provider?.fingerprint ?? "absent"}`);
|
|
1053
|
+
}
|
|
1054
|
+
if (environmentDrift) {
|
|
1055
|
+
observedProvider = sha256(`environment-drift\0${observedProvider}`);
|
|
1056
|
+
}
|
|
964
1057
|
return {
|
|
965
1058
|
profileId: manifest.profileId,
|
|
966
1059
|
workspaceId: manifest.workspaceId,
|
|
@@ -1172,6 +1265,9 @@ export async function runRuntimeHelperRequest({ requestPath, config }) {
|
|
|
1172
1265
|
generation: request.generation,
|
|
1173
1266
|
};
|
|
1174
1267
|
}
|
|
1268
|
+
if (request.action === "INSTALL_PROVIDER_CREDENTIAL") {
|
|
1269
|
+
return installProviderCredential(profile, request, config);
|
|
1270
|
+
}
|
|
1175
1271
|
if (request.action === "INSTALL_RUNTIME_SERVICE_ENV") {
|
|
1176
1272
|
return installRuntimeServiceEnvironment(profile, request);
|
|
1177
1273
|
}
|
|
@@ -1187,6 +1283,7 @@ export async function runRuntimeHelperRequest({ requestPath, config }) {
|
|
|
1187
1283
|
rmSync(target, { recursive: true, force: true });
|
|
1188
1284
|
fsyncDirectory(root);
|
|
1189
1285
|
}
|
|
1286
|
+
removeProviderCredential(profile, request.profileId);
|
|
1190
1287
|
return { ok: true, profileId: request.profileId, disposition: "REMOVED" };
|
|
1191
1288
|
}
|
|
1192
1289
|
if (request.action === "CHOWN_PROFILE") {
|
|
@@ -247,6 +247,8 @@ function validateInstallInput(input) {
|
|
|
247
247
|
profile.hermesSourceRoot !== `${EXTERNAL_RUNTIME_CLOSURE_ROOT}/hermes` ||
|
|
248
248
|
profile.proxyConfigPath !== "/etc/sellable-agent/egress-proxy.json" ||
|
|
249
249
|
profile.runtimeBoundaryConfigPath !== "/etc/sellable-agent/runtime-boundary.json" ||
|
|
250
|
+
profile.providerCredentialSourcePath !== "/var/lib/sellable-agent-bootstrap/provider-auth" ||
|
|
251
|
+
!SHA256.test(profile.providerCredentialFingerprint ?? "") ||
|
|
250
252
|
profile.runtimeServicePath !== "/etc/services.d/sellable-agent-runtime" ||
|
|
251
253
|
profile.runtimeServiceEnvRoot !== "/etc/services.d/sellable-agent-runtime/env" ||
|
|
252
254
|
new Set([profile.runtimeUid, profile.proxyUid, profile.workerUid]).size !== 3
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sellable/install",
|
|
3
|
-
"version": "0.1.362
|
|
3
|
+
"version": "0.1.362",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "One-command installer for Sellable MCP in Claude Code, Codex, and Hermes",
|
|
6
6
|
"bin": {
|
|
7
7
|
"sellable": "bin/sellable-install.mjs",
|
|
8
|
+
"sellable-agent-host-bootstrap": "bin/sellable-agent-host-bootstrap.mjs",
|
|
8
9
|
"sellable-agent-egress-proxy": "bin/sellable-agent-egress-proxy.mjs",
|
|
9
10
|
"sellable-agent-host-worker": "bin/sellable-agent-host-worker.mjs",
|
|
10
11
|
"sellable-agent-runtime-helper": "bin/sellable-agent-runtime-helper.mjs",
|
|
@@ -20,6 +21,7 @@
|
|
|
20
21
|
"bin",
|
|
21
22
|
"lib",
|
|
22
23
|
"services",
|
|
24
|
+
"container",
|
|
23
25
|
"agents",
|
|
24
26
|
"skill-templates",
|
|
25
27
|
"README.md",
|
|
@@ -17,8 +17,3 @@ Use `refill-sends-v2` instead:
|
|
|
17
17
|
```text
|
|
18
18
|
get_subskill_prompt({ subskillName: "refill-sends-v2" })
|
|
19
19
|
```
|
|
20
|
-
|
|
21
|
-
The alias must not strip exact `targetConfig`, server `runHandle`,
|
|
22
|
-
`reportingContext`, canonical `refill_reporting.v2`, or an explicit
|
|
23
|
-
`messageTemplateRevision`. It never infers revision authority from
|
|
24
|
-
`forceRerun`.
|
|
@@ -52,35 +52,9 @@ refill_sends_v2({ workspaceId, dryRun:true, intent:"auto", senderIds? })
|
|
|
52
52
|
To resume an in-progress run, pass the handle back exactly:
|
|
53
53
|
|
|
54
54
|
```text
|
|
55
|
-
refill_sends_v2({ workspaceId,
|
|
55
|
+
refill_sends_v2({ workspaceId, runId, fence })
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
`targetConfig` and the server-issued `runHandle` are separate authorities. Keep
|
|
59
|
-
both byte-for-byte through every continuation and takeover; never fold the
|
|
60
|
-
rotating fence into target configuration.
|
|
61
|
-
|
|
62
|
-
An approved copy change is reachable only through an explicit
|
|
63
|
-
`messageTemplateRevision` object with `version:1`, `source:"user_approved"`,
|
|
64
|
-
the approved `templateMarkdown`, `messageTemplateRevision` identity,
|
|
65
|
-
`priorTemplateAuthorityDigest`, `nextTemplateAuthorityDigest`, 1–500 exact
|
|
66
|
-
`cohortRowIds`, and `requestId`, `effectId`, and `revisionOperationId`. Pass it
|
|
67
|
-
with the exact `targetConfig` and `reportingContext`; the command obtains or
|
|
68
|
-
resumes the server run handle and lets the planner name
|
|
69
|
-
`revise_message_template_and_rerun`. Missing or partial input means no revision.
|
|
70
|
-
`forceRerun:true` on a queue operation never authorizes or implies a template
|
|
71
|
-
change.
|
|
72
|
-
|
|
73
|
-
After a committed and dispatched revision receipt, continue only with the
|
|
74
|
-
planner-provided revision-derived `approve_messages` action. It must preserve
|
|
75
|
-
the exact revision cohort as `rowSelector:{type:"rowIds",rowIds:[...]}` plus
|
|
76
|
-
`approvalMode:"approve"`, the compiled readiness identity, and the canonical
|
|
77
|
-
`reportingContext`, even when the ordinary sender lane is ineligible. Do not
|
|
78
|
-
replace it with `queue_campaign_cells` on Approved cells; that path cannot mint
|
|
79
|
-
the authoritative approval/lane-scope receipt. Require `laneScope` in the
|
|
80
|
-
bounded preparation receipt before the lane is considered approved. A
|
|
81
|
-
disconnected or zero-capacity sender still blocks scheduling and does not
|
|
82
|
-
authorize reconnecting, campaign start, or direct send.
|
|
83
|
-
|
|
84
58
|
If a stale handle loses the lease, the tool reports the holder status and the
|
|
85
59
|
approximately 10 minute lockout window. Reinvoke with the current handle or wait
|
|
86
60
|
for lease expiry; never guess a fence.
|
|
@@ -111,10 +85,6 @@ The run may execute only packet-named bounded work:
|
|
|
111
85
|
when the campaign is in the packet's pinned lane chain;
|
|
112
86
|
- refresh paid InMail credit facts during bootstrap and once per scheduler-wait
|
|
113
87
|
entry when the packet requires it.
|
|
114
|
-
- apply `revise_message_template_and_rerun` only when the packet contains the
|
|
115
|
-
complete explicit user-approved revision envelope and exact fenced target.
|
|
116
|
-
- after that revision commits, apply only its exact-row, readiness-bound
|
|
117
|
-
`approve_messages` continuation and require the returned lane-scope receipt.
|
|
118
88
|
|
|
119
89
|
The loop records planned -> did -> outcome before and after every foreign
|
|
120
90
|
mutation. It refuses stale packets by fingerprint and validates packet
|
|
@@ -198,10 +168,5 @@ reason or resume handle, lane source, lane chain, chosen label with token
|
|
|
198
168
|
secondary, plan revision, journal path, blocked continuation packets, and
|
|
199
169
|
firstFailing checklist when present.
|
|
200
170
|
|
|
201
|
-
Preserve `refill_reporting.v2` exactly in progress, continuation, replay, and
|
|
202
|
-
terminal results. Do not recompute its semantic counts or drop live-preparation
|
|
203
|
-
epoch, snapshot sequence, reset reason, watermark, blockers, compatibility, or
|
|
204
|
-
terminal fields. Never surface raw prospect or message-copy fields.
|
|
205
|
-
|
|
206
171
|
Real-run journals live under `~/.sellable/refill/runs` by default and append an
|
|
207
172
|
index line. Dry runs include the dry marker and do not create run records.
|
|
@@ -93,7 +93,7 @@ Accepted invocation flags in the same user request:
|
|
|
93
93
|
When the host can call typed MCP tools, start with:
|
|
94
94
|
|
|
95
95
|
```text
|
|
96
|
-
refill_sends({ yolo?: boolean, executionMode?: "manual" | "scheduled" | "yolo", requireWorkspace?: boolean, workspaceId?: string, senders?: string[], senderIds?: string[], senderNames?: string[], actionTypes?: ("send_invite" | "send_inmail_closed")[], horizonSendDays?: number, untilDate?: "YYYY-MM-DD", targetDate?: "YYYY-MM-DD",
|
|
96
|
+
refill_sends({ yolo?: boolean, executionMode?: "manual" | "scheduled" | "yolo", requireWorkspace?: boolean, workspaceId?: string, senders?: string[], senderIds?: string[], senderNames?: string[], actionTypes?: ("send_invite" | "send_inmail_closed")[], horizonSendDays?: number, untilDate?: "YYYY-MM-DD", targetDate?: "YYYY-MM-DD", runId?: string, fence?: number })
|
|
97
97
|
```
|
|
98
98
|
|
|
99
99
|
That command helper normalizes arguments and returns the execution contract. In
|
|
@@ -104,33 +104,6 @@ host must immediately call `refill_sends` again with the original full scope
|
|
|
104
104
|
plus the exact returned `runId` and `fence`. Do this automatically inside the
|
|
105
105
|
same user command until terminal projected coverage or a concrete blocker; do
|
|
106
106
|
not ask the user to type `continue` and do not start a second run.
|
|
107
|
-
|
|
108
|
-
`targetConfig` and the server-issued `runHandle` are separate values and must
|
|
109
|
-
survive each continuation unchanged except for a server-issued takeover fence.
|
|
110
|
-
Only a literal `messageTemplateRevision` with `source:"user_approved"`, the
|
|
111
|
-
approved `templateMarkdown`, prior/new authority digests, 1–500 exact cohort
|
|
112
|
-
row IDs, and request/effect/revision-operation IDs can authorize
|
|
113
|
-
`revise_message_template_and_rerun`. No envelope means no revision. A
|
|
114
|
-
`forceRerun:true` queue call cannot infer or authorize template mutation.
|
|
115
|
-
When the user supplied a complete revision except for the current prior digest,
|
|
116
|
-
acquire the exact run first and reread with its unchanged `targetConfig` and
|
|
117
|
-
`runHandle`. Use only `reportingContext.source.templateAuthorityDigest` from
|
|
118
|
-
that fenced reread as `priorTemplateAuthorityDigest`, then immediately continue
|
|
119
|
-
the same run with the complete literal revision—even when the ordinary refill
|
|
120
|
-
queue is empty or the sender is otherwise ineligible. An empty ordinary queue
|
|
121
|
-
must not suppress an explicit revision. If the fenced reread returns no digest,
|
|
122
|
-
stop with `template_authority_missing` or the returned
|
|
123
|
-
`templateAuthorityBlocker`; never guess the digest.
|
|
124
|
-
After the revision receipt is committed and dispatched, the next mutation must
|
|
125
|
-
be the planner-provided revision-derived `approve_messages` action. Preserve
|
|
126
|
-
its exact `rowSelector:{type:"rowIds",rowIds:[...]}` cohort,
|
|
127
|
-
`approvalMode:"approve"`, compiled readiness authority, and
|
|
128
|
-
`reportingContext`, even if the ordinary sender lane is ineligible. Never use
|
|
129
|
-
`queue_campaign_cells` to flip Approved cells: that path cannot produce the
|
|
130
|
-
authoritative approval/lane-scope receipt. Require the bounded preparation
|
|
131
|
-
receipt's `laneScope` before considering the selected first-touch lane
|
|
132
|
-
approved. Zero sender capacity remains a scheduler blocker and never authorizes
|
|
133
|
-
reconnection, campaign start, or direct send.
|
|
134
107
|
Safe primitives are paid-credit refresh,
|
|
135
108
|
existing-row message preparation, generated-message approval, receipt-proven
|
|
136
109
|
same-source row copy, one bounded Signal Discovery search derived only from
|
|
@@ -594,11 +567,6 @@ scheduler wait loop. Do not finish the refill goal, mark it complete, or mark it
|
|
|
594
567
|
blocked only because it is still `awaiting_scheduler_after_ready_buffer`; keep
|
|
595
568
|
waiting unless Christian stops the run or the host cannot continue.
|
|
596
569
|
|
|
597
|
-
Carry the canonical `refill_reporting.v2` DTO through progress, continuation,
|
|
598
|
-
replay, and terminal reporting without recomputing counts or losing live epoch,
|
|
599
|
-
snapshot sequence, reset reason, watermark, blockers, compatibility, or
|
|
600
|
-
terminal outcome. Redact raw copy and prospect fields.
|
|
601
|
-
|
|
602
570
|
Future scheduled coverage and already sent actions are distinct; only
|
|
603
571
|
scheduler-owned future scheduled actions count as scheduled coverage. For
|
|
604
572
|
multiple target dates, finish D1 execution and the full D1 reread before
|