@sellable/install 0.1.777 → 0.1.779
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/container/Dockerfile +4 -4
- package/container/README.md +1 -1
- package/lib/sellable-agent/default-profile-bundles/shared/manifest.fragment.json +1 -1
- package/lib/sellable-agent/default-profile-bundles/shared/skills/sellable/sellable-campaign-daily-review/SKILL.md +9 -6
- package/lib/sellable-agent/fly-admin-image/Dockerfile +1 -1
- package/lib/sellable-agent/fly-admin-image/admin-runtime.mjs +2 -2
- package/lib/sellable-agent/fly-customer-image/Dockerfile +2 -2
- package/lib/sellable-agent/fly-customer-image/customer-runtime.mjs +10 -1
- package/lib/sellable-agent/hermes-memory-snapshot.mjs +24 -17
- package/lib/sellable-agent/host-bootstrap.mjs +1 -1
- package/lib/sellable-agent/host-worker.mjs +1 -1
- package/lib/sellable-agent/profile-materializer.mjs +1 -1
- package/lib/sellable-agent/provisioning-adapter.mjs +1 -1
- package/lib/sellable-agent/public-skills.json +1 -1
- package/package.json +1 -1
- package/skill-templates/campaign-daily-review.md +9 -6
package/container/Dockerfile
CHANGED
|
@@ -3,12 +3,12 @@ FROM ${HERMES_IMAGE}
|
|
|
3
3
|
|
|
4
4
|
USER root
|
|
5
5
|
|
|
6
|
-
ARG INSTALLER_PACKAGE=@sellable/install@0.1.
|
|
6
|
+
ARG INSTALLER_PACKAGE=@sellable/install@0.1.779
|
|
7
7
|
ARG MCP_PACKAGE=@sellable/mcp@0.1.947
|
|
8
8
|
ARG INSTALLER_INTEGRITY
|
|
9
9
|
ARG MCP_INTEGRITY=sha512-3/IoWRHWyTjdh7OjYHdYS+Na6v9TkYhGXKSYX4rYjhqm4b7MtyV2HaRlZVYKUv8afS1UzZcCUXUx7kqF/Mzk4A==
|
|
10
10
|
|
|
11
|
-
RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.
|
|
11
|
+
RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.779" \
|
|
12
12
|
&& test "${MCP_PACKAGE}" = "@sellable/mcp@0.1.947" \
|
|
13
13
|
&& test -n "${INSTALLER_INTEGRITY}" \
|
|
14
14
|
&& test "$(npm view "${INSTALLER_PACKAGE}" dist.integrity)" = "${INSTALLER_INTEGRITY}" \
|
|
@@ -23,9 +23,9 @@ RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.777" \
|
|
|
23
23
|
&& mkdir -p /opt/sellable-agent/install-root /opt/sellable-agent/mcp-root \
|
|
24
24
|
&& npm install --prefix /opt/sellable-agent/install-root --omit=dev --ignore-scripts --no-audit --no-fund "${INSTALLER_PACKAGE}" \
|
|
25
25
|
&& npm install --prefix /opt/sellable-agent/mcp-root --omit=dev --ignore-scripts --no-audit --no-fund "${MCP_PACKAGE}" \
|
|
26
|
-
&& node --input-type=module -e "import { readFileSync } from 'node:fs'; const install = JSON.parse(readFileSync('/opt/sellable-agent/install-root/node_modules/@sellable/install/package.json')); const mcp = JSON.parse(readFileSync('/opt/sellable-agent/mcp-root/node_modules/@sellable/mcp/package.json')); if (install.name !== '@sellable/install' || install.version !== '0.1.
|
|
26
|
+
&& node --input-type=module -e "import { readFileSync } from 'node:fs'; const install = JSON.parse(readFileSync('/opt/sellable-agent/install-root/node_modules/@sellable/install/package.json')); const mcp = JSON.parse(readFileSync('/opt/sellable-agent/mcp-root/node_modules/@sellable/mcp/package.json')); if (install.name !== '@sellable/install' || install.version !== '0.1.779' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.947') throw new Error('package identity rejected');" \
|
|
27
27
|
&& node --input-type=module -e "import { buildExternalRuntimeClosure } from '/opt/sellable-agent/install-root/node_modules/@sellable/install/lib/sellable-agent/external-runtime-builder.mjs'; const built = buildExternalRuntimeClosure({ mcpPackageRoot: '/opt/sellable-agent/mcp-root/node_modules/@sellable/mcp', mcpNodeModulesRoot: '/opt/sellable-agent/mcp-root/node_modules', hermesSourceRoot: '/opt/hermes', ownerUid: 0, ownerGid: 0 }); if (!built.closureDigest) throw new Error('runtime closure rejected');" \
|
|
28
|
-
&& node --input-type=module -e "import { chmodSync, writeFileSync } from 'node:fs'; const release = { installerPackage: '@sellable/install@0.1.
|
|
28
|
+
&& node --input-type=module -e "import { chmodSync, writeFileSync } from 'node:fs'; const release = { installerPackage: '@sellable/install@0.1.779', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.947', mcpIntegrity: process.argv[2] }; writeFileSync('/opt/sellable-agent/release.json', JSON.stringify(release) + '\\n', { mode: 0o444 }); chmodSync('/opt/sellable-agent/release.json', 0o444);" "${INSTALLER_INTEGRITY}" "${MCP_INTEGRITY}" \
|
|
29
29
|
&& npm cache clean --force
|
|
30
30
|
|
|
31
31
|
COPY entrypoint.sh /usr/local/bin/sellable-agent-container-entrypoint
|
package/container/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
This image is a dedicated Agent worker/runtime boundary beside an existing Hermes dashboard. It never replaces or reconfigures the dashboard container.
|
|
4
4
|
|
|
5
5
|
Build from this directory after
|
|
6
|
-
`@sellable/install@0.1.
|
|
6
|
+
`@sellable/install@0.1.779` is
|
|
7
7
|
published:
|
|
8
8
|
|
|
9
9
|
```sh
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"ownership": "system_managed",
|
|
30
30
|
"path": "skills/sellable/sellable-campaign-daily-review/SKILL.md",
|
|
31
31
|
"mode": "0444",
|
|
32
|
-
"content": "---\nname: sellable-campaign-daily-review\ndescription: Review existing Sellable campaigns and move exactly one useful next step forward.\nvisibility: public\nallowed-tools:\n - mcp_sellable_get_auth_status\n - mcp_sellable_customer_program\n - mcp_sellable_get_campaigns\n - mcp_sellable_get_campaign\n - mcp_sellable_get_campaign_context\n - mcp_sellable_get_campaign_refill_state\n - mcp_sellable_refill_sends\n---\n\n# Sellable Campaign Daily Review\n\n## Installed Host Contract\n\nThis installed skill is running in Hermes Agent. When the shared workflow body\nor fallback text mentions Claude Code, Codex, or Hermes for internal parity,\nchoose the Hermes instruction for customer-facing language and host functions.\n\n- Customer-facing command: `/sellable-campaign-daily-review`\n- MCP tool naming: Hermes exposes Sellable tools as `mcp_sellable_<tool>`;\n when shared instructions show `mcp_sellable_<tool>`, call the matching\n `mcp_sellable_<tool>` tool instead.\n- Structured questions: ask plainly in chat unless a Hermes-native approval or\n question tool is visible in the current session.\n- Bootstrap host label: `host: \"Hermes\"`\n- Install/reload blocker label: Hermes install/reload problem\n- Reload instruction: restart Hermes, or run `/reload-mcp` in the active\n Hermes session after install\n\nDo not tell Hermes users to run Codex or Claude command forms, use Codex/Claude structured-question APIs, or restart Codex Desktop or Claude Code. Do not describe this run as Claude Code or Codex.\n\nHelp the customer keep the campaign they already have healthy and moving. This\nis one continuous workflow: understand current workspace state, choose one\nuseful next step, use the existing campaign owner, observe the result, and save\nthe exact wait or outcome.\n\n## Run the review\n\n1. Call `get_auth_status`, then obtain a fenced `customer_program` review claim\n and load the complete home-workspace campaign inventory. For a scheduled\n `execute` or `resume` wake, use the scheduled trigger and the wake gate's\n exact lease owner. For a direct customer request, use the interactive\n trigger. For a `reconcile` wake, skip campaign work and follow the delivery\n recovery below. Keep each strict tool input separate: claim with only\n `action`, `trigger`, and `leaseOwner`; never add the wake's `effectId` or\n `fence` to the claim call.\n2. Infer the relationship stage from that workspace state. Keep campaign health\n and wait state separate from the relationship stage.\n3. Assess each campaign from approved ICP, source, message and actual outcome\n evidence. `ACTIVE` or a full queue does not prove suitability. Sparse results\n do not prove poor quality. Unavailable data is not an empty workspace.\n4. Inspect supply and capacity, then choose exactly one outcome: `maintain`,\n `refill`, `continue`, `edit`, `propose_create`, `input`, or `blocker`.\n5. Execute through the existing owner:\n - suitable campaign with a real supply gap: call canonical Refill Sends with `yolo:true` and keep its returned run token. Keep going until the gap is filled or a real blocker remains; when a source runs dry, refill widens it. Do not stop to ask the customer for leads;\n - unfinished campaign: continue the same campaign through Create Campaign;\n - supported quality issue: edit the same campaign through Create Campaign;\n - no campaign: enter Create Campaign, asking only for the LinkedIn profile\n first when it is missing;\n - healthy supplied campaign: maintain it and stay quiet;\n - manual pause: respect it;\n - missing approval, connection or decision: with `launchConsent.mode: \"autopilot\"` approve messages that passed the quality check and start or resume the campaign yourself
|
|
32
|
+
"content": "---\nname: sellable-campaign-daily-review\ndescription: Review existing Sellable campaigns and move exactly one useful next step forward.\nvisibility: public\nallowed-tools:\n - mcp_sellable_get_auth_status\n - mcp_sellable_customer_program\n - mcp_sellable_get_campaigns\n - mcp_sellable_get_campaign\n - mcp_sellable_get_campaign_context\n - mcp_sellable_get_campaign_refill_state\n - mcp_sellable_refill_sends\n---\n\n# Sellable Campaign Daily Review\n\n## Installed Host Contract\n\nThis installed skill is running in Hermes Agent. When the shared workflow body\nor fallback text mentions Claude Code, Codex, or Hermes for internal parity,\nchoose the Hermes instruction for customer-facing language and host functions.\n\n- Customer-facing command: `/sellable-campaign-daily-review`\n- MCP tool naming: Hermes exposes Sellable tools as `mcp_sellable_<tool>`;\n when shared instructions show `mcp_sellable_<tool>`, call the matching\n `mcp_sellable_<tool>` tool instead.\n- Structured questions: ask plainly in chat unless a Hermes-native approval or\n question tool is visible in the current session.\n- Bootstrap host label: `host: \"Hermes\"`\n- Install/reload blocker label: Hermes install/reload problem\n- Reload instruction: restart Hermes, or run `/reload-mcp` in the active\n Hermes session after install\n\nDo not tell Hermes users to run Codex or Claude command forms, use Codex/Claude structured-question APIs, or restart Codex Desktop or Claude Code. Do not describe this run as Claude Code or Codex.\n\nHelp the customer keep the campaign they already have healthy and moving. This\nis one continuous workflow: understand current workspace state, choose one\nuseful next step, use the existing campaign owner, observe the result, and save\nthe exact wait or outcome.\n\n## Run the review\n\n1. Call `get_auth_status`, then obtain a fenced `customer_program` review claim\n and load the complete home-workspace campaign inventory. For a scheduled\n `execute` or `resume` wake, use the scheduled trigger and the wake gate's\n exact lease owner. For a direct customer request, use the interactive\n trigger. For a `reconcile` wake, skip campaign work and follow the delivery\n recovery below. Keep each strict tool input separate: claim with only\n `action`, `trigger`, and `leaseOwner`; never add the wake's `effectId` or\n `fence` to the claim call.\n2. Infer the relationship stage from that workspace state. Keep campaign health\n and wait state separate from the relationship stage.\n3. Assess each campaign from approved ICP, source, message and actual outcome\n evidence. `ACTIVE` or a full queue does not prove suitability. Sparse results\n do not prove poor quality. Unavailable data is not an empty workspace.\n4. Inspect supply and capacity, then choose exactly one outcome: `maintain`,\n `refill`, `continue`, `edit`, `propose_create`, `input`, or `blocker`.\n5. Execute through the existing owner:\n - suitable campaign with a real supply gap: call canonical Refill Sends with `yolo:true` and keep its returned run token. Keep going until the gap is filled or a real blocker remains; when a source runs dry, refill widens it. Do not stop to ask the customer for leads;\n - unfinished campaign: continue the same campaign through Create Campaign;\n - supported quality issue: edit the same campaign through Create Campaign;\n - no campaign: enter Create Campaign, asking only for the LinkedIn profile\n first when it is missing;\n - healthy supplied campaign: maintain it and stay quiet;\n - manual pause: respect it;\n - missing approval, connection or decision: with `launchConsent.mode: \"autopilot\"` approve messages that passed the quality check and start or resume the campaign yourself. When `launchConsent` has no answer yet, ask the one-time question instead of asking for approval: \"I can approve messages that pass my quality check and launch for you, then keep your sends filled. Reply review first if you would rather approve.\" Save the answer with `save_context` as Create Campaign's Standing Consent does. With `review_first`, retain one exact next step. Ask only for what you cannot do yourself, such as a LinkedIn connection, as one question.\n A built-in weekday refill job fills sends before this review; do not run\n Refill Sends again unless `refill` is this review's one outcome. Read\n `refill_v3_world_state` for the next two sender-local days: a row whose\n reason names a disconnected sender (`sender_disconnected`), a disconnected\n Sales Navigator (`sales_nav_disconnected`) or\n `paid_inmail_connection_health_recheck_required` is a fix only the customer\n can make. Make it this review's `input`: name the sender and say reconnect\n LinkedIn or reconnect Sales Navigator in Settings.\n6. Observe the owned operation, then record the outcome against the same review\n effect and fence. The `record_review_outcome` call contains only its action,\n effect, fence, review outcome, summary, and optional decision/operation refs;\n do not include trigger or lease fields. Never mark a pending or uncertain\n effect complete. On a scheduled actionable outcome, saving the outcome is\n not completion. Inspect `deliveryRequired`; when it is `true`, continue\n immediately to the required `stage_delivery` call. Never return while the recorded run still has `deliveryState: \"pending\"`. Write the outcome `summary` in plain customer words with the verified numbers (leads added, messages approved, sends scheduled): the next results update reads it as \"What Sellable did\".\n\n## Write the Slack message\n\nPost only new work or a real need, never a status. Use real line breaks: a bold title, a blank line between sections, one item per line, never one long paragraph. Write ordinary Markdown; delivery converts it for Slack.\n\n**{Workspace} · Campaign update · {Weekday Mon D}**\n\n**What I did** says in plain words what changed: leads added, messages approved,\nsends scheduled, campaign started, or a source widened.\n\n**Next step** and **Options** appear only when the customer must act: one specific\naction, then at most two options they can reply with. Offer only what you can really\ndo (refill, continue or edit the campaign) or what they can do (add leads, approve\nmessages, reconnect a sender).\n\nLead with what Sellable did to grow their sends. Never tell the customer what they\nhave not done, and never post a scoreboard of zeros. Plain words: no run tokens,\njob or effect IDs, error codes, tool names or file paths. Unknown is never zero.\nAbout 20-60 words.\n\n## Stop and delivery rules\n\nAn internal system fault (a run token, an error code, refill machinery) is not\na customer decision or blocker: do not post it.\n\nIf a tool call fails or its result can't be read, return `[SILENT]` and stop.\nNever post an error, an apology or a guess; the open claim lets the next\nscheduled window retry.\n\nStop on settled state, a human wait, a retained async wait, a typed blocker or\nthe existing bounded operation budget. Never restart a pending effect. Follow a\nnew customer reply or product completion immediately instead of waiting for the\nnext daily tick.\n\nPost a useful new decision or blocker once to the verified current main channel.\nRoutine refill belongs in the next results brief. A healthy no-op is inspectable\nand silent. On a scheduled actionable outcome, compose the complete final Slack\ntext, call `customer_program` with only `action: \"stage_delivery\"`, the exact\neffect and fence, and `text`, then inspect `delivery.deliverNow`. Only when it is `true`, return\n`delivery.text` verbatim as the native job's final response. When it is\n`false`, return `[SILENT]`; that effect is already uncertain or delivered,\nand the managed reconciliation tick owns the next step. Do not stage an interactive response.\n\nBefore any scheduled final response, check the recorded run one last time. A\n`pending` delivery is an unfinished cron turn: call `stage_delivery` now. Only\n`accepted_uncertain`, `delivered`, `failed`, or `not_applicable` may end the\nturn.\n\nOn a scheduled `reconcile` wake, call `reconcile_delivery` with only its action\nand the exact effect.\nIf trusted Slack history confirms the text, or retry is not yet allowed, return\n`[SILENT]`. If retry is allowed, call `retry_delivery` with the current fence and\nreturn its stored `delivery.text` verbatim. Never regenerate it. The native\nmanaged job remains the only Slack sender; the product owns the claim and\ndelivery ledger. After three unconfirmed attempts the product closes the run as\nfailed and later ticks stay silent. Never create another post path or fall back\nto another channel.\n\nDo not create a second campaign engine, refill algorithm, scheduler, scoring\nframework, intake questionnaire or generic morning approval. Do not rewrite\nstrategy or messages merely because another day passed. Initial campaign launch\nstill requires the existing human Start.\n"
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"id": "campaign-daily-results",
|
|
@@ -63,12 +63,15 @@ the exact wait or outcome.
|
|
|
63
63
|
first when it is missing;
|
|
64
64
|
- healthy supplied campaign: maintain it and stay quiet;
|
|
65
65
|
- manual pause: respect it;
|
|
66
|
-
- missing approval, connection or decision: with `launchConsent.mode: "autopilot"` approve messages that passed the quality check and start or resume the campaign yourself
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
- missing approval, connection or decision: with `launchConsent.mode: "autopilot"` approve messages that passed the quality check and start or resume the campaign yourself. When `launchConsent` has no answer yet, ask the one-time question instead of asking for approval: "I can approve messages that pass my quality check and launch for you, then keep your sends filled. Reply review first if you would rather approve." Save the answer with `save_context` as Create Campaign's Standing Consent does. With `review_first`, retain one exact next step. Ask only for what you cannot do yourself, such as a LinkedIn connection, as one question.
|
|
67
|
+
A built-in weekday refill job fills sends before this review; do not run
|
|
68
|
+
Refill Sends again unless `refill` is this review's one outcome. Read
|
|
69
|
+
`refill_v3_world_state` for the next two sender-local days: a row whose
|
|
70
|
+
reason names a disconnected sender (`sender_disconnected`), a disconnected
|
|
71
|
+
Sales Navigator (`sales_nav_disconnected`) or
|
|
72
|
+
`paid_inmail_connection_health_recheck_required` is a fix only the customer
|
|
73
|
+
can make. Make it this review's `input`: name the sender and say reconnect
|
|
74
|
+
LinkedIn or reconnect Sales Navigator in Settings.
|
|
72
75
|
6. Observe the owned operation, then record the outcome against the same review
|
|
73
76
|
effect and fence. The `record_review_outcome` call contains only its action,
|
|
74
77
|
effect, fence, review outcome, summary, and optional decision/operation refs;
|
|
@@ -44,7 +44,7 @@ ARG STRIPE_CLI_PACKAGE=@stripe/cli-linux-x64@1.45.0
|
|
|
44
44
|
ARG STRIPE_CLI_INTEGRITY=sha512-1ZhoPpoweYfynqsvhCLlTjZ7lPz5IKtxqAMT5MBy6zrwLRTP9x0T6r8h/jLYL8lRH1c7zC/6X6/XUu2V+r9Bog==
|
|
45
45
|
ARG DEFAULT_PROFILE_BUNDLE_FIXTURE_VERSION=1
|
|
46
46
|
ARG DEFAULT_PROFILE_BUNDLE_VERSION=4
|
|
47
|
-
ARG DEFAULT_PROFILE_BUNDLE_DIGEST=
|
|
47
|
+
ARG DEFAULT_PROFILE_BUNDLE_DIGEST=e24677e470c289ac5b27d279693deca5fba10b8ff1d02b218bf523cc39bc1a13
|
|
48
48
|
|
|
49
49
|
COPY --from=slack-pp-cli-builder --chmod=0555 /out/slack-pp-cli /usr/local/bin/slack-pp-cli
|
|
50
50
|
|
|
@@ -88,7 +88,7 @@ import {
|
|
|
88
88
|
HERMES_SNAPSHOT_HOOK_ALLOWLIST_BYTES,
|
|
89
89
|
applyHermesSellableHostedPolicy,
|
|
90
90
|
ensureHermesClientRelationshipsCrons,
|
|
91
|
-
|
|
91
|
+
ensureHermesRefillSendsCron,
|
|
92
92
|
ensureHermesCustomerProgramCrons,
|
|
93
93
|
ensureHermesOpportunitySequencesCron,
|
|
94
94
|
ensureHermesSnapshotCron,
|
|
@@ -1442,7 +1442,7 @@ async function materializeAdminProfile({
|
|
|
1442
1442
|
// NON-FATAL like its siblings: a missed refill tick is harmless (refill
|
|
1443
1443
|
// no-ops when queues are full) and must not block an Admin promotion.
|
|
1444
1444
|
const adminRefillCron = slack
|
|
1445
|
-
? await
|
|
1445
|
+
? await ensureHermesRefillSendsCron({
|
|
1446
1446
|
profileId: profile,
|
|
1447
1447
|
profileRoot: profileHome,
|
|
1448
1448
|
hermesRoot: DATA_ROOT,
|
|
@@ -8,7 +8,7 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/opt/hermes/.playwright
|
|
|
8
8
|
|
|
9
9
|
ARG MCP_PACKAGE=@sellable/mcp@0.1.947
|
|
10
10
|
ARG MCP_INTEGRITY=sha512-3/IoWRHWyTjdh7OjYHdYS+Na6v9TkYhGXKSYX4rYjhqm4b7MtyV2HaRlZVYKUv8afS1UzZcCUXUx7kqF/Mzk4A==
|
|
11
|
-
ARG INSTALLER_PACKAGE=@sellable/install@0.1.
|
|
11
|
+
ARG INSTALLER_PACKAGE=@sellable/install@0.1.779
|
|
12
12
|
ARG ADMIN_MCP_PACKAGE=@sellable/admin-mcp@0.1.202
|
|
13
13
|
ARG ADMIN_MCP_INTEGRITY=sha512-ym3aye/0vQ8tfULJlKz9JFOxvIQsl1G+n3np+TTgK/tecwNblBo/j3AkTnRqH9iomKzPbZzP+n4tEQsvsU8ByA==
|
|
14
14
|
ARG TUS_JS_CLIENT_VERSION=4.3.1
|
|
@@ -22,7 +22,7 @@ ARG PLAYWRIGHT_BROWSER_REVISION=1243
|
|
|
22
22
|
ARG TIRITH_ARCHIVE_SHA256=6cdbe35e8f9ccf42e70ad95b501c93cd218ac18201c3df958d54f6ba0d995ce2
|
|
23
23
|
ARG DEFAULT_PROFILE_BUNDLE_FIXTURE_VERSION=1
|
|
24
24
|
ARG DEFAULT_PROFILE_BUNDLE_VERSION=4
|
|
25
|
-
ARG DEFAULT_PROFILE_BUNDLE_DIGEST=
|
|
25
|
+
ARG DEFAULT_PROFILE_BUNDLE_DIGEST=bd97b34dc228ee1d91deaafc52283f555e34165786abba26bad398202c6e087c
|
|
26
26
|
|
|
27
27
|
RUN --mount=type=secret,id=mcp_trust_root,required=true \
|
|
28
28
|
test -s /run/secrets/mcp_trust_root \
|
|
@@ -54,6 +54,7 @@ import { ensureHermesFirstWelcome } from "../hermes-first-welcome.mjs";
|
|
|
54
54
|
import {
|
|
55
55
|
ensureHermesCustomerProgramCrons,
|
|
56
56
|
ensureHermesFirstCampaignOnboardingCron,
|
|
57
|
+
ensureHermesRefillSendsCron,
|
|
57
58
|
ensureHermesSnapshotCron,
|
|
58
59
|
markHermesSnapshotDirty,
|
|
59
60
|
migrateLegacyHermesUserState,
|
|
@@ -122,7 +123,7 @@ const RUNTIME_GENERATION = Number(
|
|
|
122
123
|
const BOOT_SESSION_ID = randomUUID();
|
|
123
124
|
const HERMES_VERSION = process.env.SELLABLE_AGENT_HERMES_VERSION ?? "0.21.5";
|
|
124
125
|
const INSTALLER_PACKAGE =
|
|
125
|
-
process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.
|
|
126
|
+
process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.779";
|
|
126
127
|
const MCP_PACKAGE =
|
|
127
128
|
process.env.SELLABLE_AGENT_MCP_PACKAGE ?? "@sellable/mcp@0.1.947";
|
|
128
129
|
export const ENDPOINT_MAX_CONCURRENT_RUNS = 10;
|
|
@@ -3883,6 +3884,14 @@ async function processOperation({
|
|
|
3883
3884
|
channelId: desired.defaultChannelId,
|
|
3884
3885
|
});
|
|
3885
3886
|
if (!onboardingCron.ok) throw new Error(onboardingCron.code);
|
|
3887
|
+
// NON-FATAL: a missed refill tick is harmless (refill no-ops when queues
|
|
3888
|
+
// are full and the next weekday run continues), so it never blocks serving.
|
|
3889
|
+
await ensureHermesRefillSendsCron({
|
|
3890
|
+
profileId: desired.profileId,
|
|
3891
|
+
profileRoot,
|
|
3892
|
+
hermesRoot: DATA_ROOT,
|
|
3893
|
+
workspaceId: desired.workspaceId,
|
|
3894
|
+
});
|
|
3886
3895
|
});
|
|
3887
3896
|
markHermesSnapshotDirty(profileRoot);
|
|
3888
3897
|
const onboardingMemorySnapshot = await phaseTelemetry.run(
|
|
@@ -134,13 +134,13 @@ export const HERMES_CUSTOMER_PROGRAM_CRONS = Object.freeze([
|
|
|
134
134
|
]),
|
|
135
135
|
}),
|
|
136
136
|
]);
|
|
137
|
-
// Built-in
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
|
|
137
|
+
// Built-in weekday refill on every agent (proven on the Admin image first). It
|
|
138
|
+
// posts nothing (deliver local, reply [SILENT]); the daily review names anything
|
|
139
|
+
// only the customer can fix and the results update reports what was added.
|
|
140
|
+
// Refill observes capacity itself and does nothing when queues are full, so a
|
|
141
|
+
// single weekday tick is enough and a missed one is harmless.
|
|
142
|
+
export const HERMES_REFILL_SENDS_CRON = Object.freeze({
|
|
142
143
|
name: "sellable-default-refill-sends",
|
|
143
|
-
schedule: "0 14 * * 1-5",
|
|
144
144
|
deliver: "local",
|
|
145
145
|
skills: Object.freeze(["sellable-refill-sends"]),
|
|
146
146
|
enabledToolsets: Object.freeze(["safe", "sellable", "mcp"]),
|
|
@@ -1952,17 +1952,23 @@ function opportunitySequenceIdentity(profileRoot) {
|
|
|
1952
1952
|
);
|
|
1953
1953
|
}
|
|
1954
1954
|
|
|
1955
|
-
|
|
1955
|
+
// Weekdays between 14:00 and 14:49 UTC, one stable minute per agent: every agent
|
|
1956
|
+
// starting refill at 14:00:00 crowded the product's database pool (Sep 30).
|
|
1957
|
+
export function refillSendsSchedule(profileId) {
|
|
1958
|
+
const minute = parseInt(sha256(profileId).slice(0, 8), 16) % 50;
|
|
1959
|
+
return `${minute} 14 * * 1-5`;
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
function refillSendsPrompt(workspaceId) {
|
|
1956
1963
|
return [
|
|
1957
1964
|
"Run the installed refill-sends skill with --yolo for this workspace.",
|
|
1958
1965
|
"Call refill_v3_advance with yolo true and scope {kind: sender_local_horizon, days: 2}, then continue exactly as the skill says until the run is complete, awaiting an external change, or blocked.",
|
|
1959
|
-
"
|
|
1960
|
-
"Post nothing: reply [SILENT] whatever the outcome. The next daily results update reports what was added.",
|
|
1966
|
+
"Post nothing: reply [SILENT] whatever the outcome. The daily review names anything only the customer can fix, and the next results update reports what was added.",
|
|
1961
1967
|
`workspaceId: ${workspaceId}`,
|
|
1962
1968
|
].join("\n");
|
|
1963
1969
|
}
|
|
1964
1970
|
|
|
1965
|
-
export async function
|
|
1971
|
+
export async function ensureHermesRefillSendsCron({
|
|
1966
1972
|
profileId,
|
|
1967
1973
|
profileRoot,
|
|
1968
1974
|
hermesRoot,
|
|
@@ -1975,11 +1981,12 @@ export async function ensureHermesAdminRefillSendsCron({
|
|
|
1975
1981
|
!existsSync(profileRoot) ||
|
|
1976
1982
|
!ONBOARDING_WORKSPACE_ID.test(workspaceId ?? "")
|
|
1977
1983
|
) {
|
|
1978
|
-
return failed("
|
|
1984
|
+
return failed("hermes_refill_sends_cron_identity_rejected");
|
|
1979
1985
|
}
|
|
1980
1986
|
try {
|
|
1981
|
-
const cron =
|
|
1982
|
-
const prompt =
|
|
1987
|
+
const cron = HERMES_REFILL_SENDS_CRON;
|
|
1988
|
+
const prompt = refillSendsPrompt(workspaceId);
|
|
1989
|
+
const schedule = refillSendsSchedule(profileId);
|
|
1983
1990
|
const expected = (job) =>
|
|
1984
1991
|
Boolean(
|
|
1985
1992
|
job?.id &&
|
|
@@ -1991,7 +1998,7 @@ export async function ensureHermesAdminRefillSendsCron({
|
|
|
1991
1998
|
sameJson(job.skills, cron.skills) &&
|
|
1992
1999
|
sameJson(job.enabled_toolsets, cron.enabledToolsets) &&
|
|
1993
2000
|
job.schedule?.kind === "cron" &&
|
|
1994
|
-
job.schedule?.expr ===
|
|
2001
|
+
job.schedule?.expr === schedule
|
|
1995
2002
|
);
|
|
1996
2003
|
const mutate = (jobId = null) =>
|
|
1997
2004
|
childRunner(runChild)({
|
|
@@ -2001,7 +2008,7 @@ export async function ensureHermesAdminRefillSendsCron({
|
|
|
2001
2008
|
OPPORTUNITY_SEQUENCE_CRON_PYTHON,
|
|
2002
2009
|
JSON.stringify({
|
|
2003
2010
|
prompt,
|
|
2004
|
-
schedule
|
|
2011
|
+
schedule,
|
|
2005
2012
|
name: cron.name,
|
|
2006
2013
|
deliver: cron.deliver,
|
|
2007
2014
|
skills: [...cron.skills],
|
|
@@ -2028,7 +2035,7 @@ export async function ensureHermesAdminRefillSendsCron({
|
|
|
2028
2035
|
isExpected: expected,
|
|
2029
2036
|
create: () => mutate(),
|
|
2030
2037
|
update: (job) => mutate(job.id),
|
|
2031
|
-
codePrefix: "
|
|
2038
|
+
codePrefix: "hermes_refill_sends_cron",
|
|
2032
2039
|
});
|
|
2033
2040
|
if (!result.ok) return result;
|
|
2034
2041
|
return {
|
|
@@ -2043,7 +2050,7 @@ export async function ensureHermesAdminRefillSendsCron({
|
|
|
2043
2050
|
workspaceId,
|
|
2044
2051
|
};
|
|
2045
2052
|
} catch {
|
|
2046
|
-
return failed("
|
|
2053
|
+
return failed("hermes_refill_sends_cron_install_failed");
|
|
2047
2054
|
}
|
|
2048
2055
|
}
|
|
2049
2056
|
|
|
@@ -38,7 +38,7 @@ import { installAgentHostServices } from "./service-installer.mjs";
|
|
|
38
38
|
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
39
39
|
const VERSION = "sellable-agent-host-bootstrap/v1";
|
|
40
40
|
const RECEIPT_VERSION = "sellable-agent-host-registration/v1";
|
|
41
|
-
const INSTALLER_PACKAGE = "@sellable/install@0.1.
|
|
41
|
+
const INSTALLER_PACKAGE = "@sellable/install@0.1.779";
|
|
42
42
|
const MCP_PACKAGE = "@sellable/mcp@0.1.947";
|
|
43
43
|
const HERMES_VERSION = "0.18.0";
|
|
44
44
|
const SHA256 = /^[a-f0-9]{64}$/;
|
|
@@ -1546,7 +1546,7 @@ export function compileClaimToProfileDesired(activeClaim, config) {
|
|
|
1546
1546
|
]) ||
|
|
1547
1547
|
pinned.hermesCli !== "hermes" ||
|
|
1548
1548
|
pinned.hermesVersion !== "0.18.0" ||
|
|
1549
|
-
pinned.installerPackage !== "@sellable/install@0.1.
|
|
1549
|
+
pinned.installerPackage !== "@sellable/install@0.1.779" ||
|
|
1550
1550
|
pinned.mcpPackage !== "@sellable/mcp@0.1.947" ||
|
|
1551
1551
|
!Array.isArray(policy.toolInclude) ||
|
|
1552
1552
|
policy.toolInclude.length === 0 ||
|
|
@@ -312,7 +312,7 @@ function validateDesired(desired) {
|
|
|
312
312
|
desired.serviceCredentialGeneration < 1 ||
|
|
313
313
|
desired.hermesCli !== "hermes" ||
|
|
314
314
|
desired.hermesVersion !== "0.18.0" ||
|
|
315
|
-
desired.installerPackage !== "@sellable/install@0.1.
|
|
315
|
+
desired.installerPackage !== "@sellable/install@0.1.779" ||
|
|
316
316
|
desired.mcpPackage !== "@sellable/mcp@0.1.947" ||
|
|
317
317
|
!Array.isArray(desired.toolInclude) ||
|
|
318
318
|
desired.toolInclude.length === 0 ||
|
|
@@ -25,7 +25,7 @@ import { fileURLToPath } from "node:url";
|
|
|
25
25
|
import { deriveContainedProfileId } from "./profile-materializer.mjs";
|
|
26
26
|
|
|
27
27
|
export const PROVISIONING_ACTION = "PROVISION_HERMES_PROFILE";
|
|
28
|
-
export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.
|
|
28
|
+
export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.779";
|
|
29
29
|
export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.947";
|
|
30
30
|
const PINNED_INSTALL_VERSION = PINNED_INSTALL_PACKAGE.slice(
|
|
31
31
|
PINNED_INSTALL_PACKAGE.lastIndexOf("@") + 1
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
{
|
|
47
47
|
"path": "skills/sellable/sellable-campaign-daily-review/SKILL.md",
|
|
48
|
-
"content": "---\nname: sellable-campaign-daily-review\ndescription: Review existing Sellable campaigns and move exactly one useful next step forward.\nvisibility: public\nallowed-tools:\n - mcp_sellable_get_auth_status\n - mcp_sellable_customer_program\n - mcp_sellable_get_campaigns\n - mcp_sellable_get_campaign\n - mcp_sellable_get_campaign_context\n - mcp_sellable_get_campaign_refill_state\n - mcp_sellable_refill_sends\n---\n\n# Sellable Campaign Daily Review\n\n## Installed Host Contract\n\nThis installed skill is running in Hermes Agent. When the shared workflow body\nor fallback text mentions Claude Code, Codex, or Hermes for internal parity,\nchoose the Hermes instruction for customer-facing language and host functions.\n\n- Customer-facing command: `/sellable-campaign-daily-review`\n- MCP tool naming: Hermes exposes Sellable tools as `mcp_sellable_<tool>`;\n when shared instructions show `mcp_sellable_<tool>`, call the matching\n `mcp_sellable_<tool>` tool instead.\n- Structured questions: ask plainly in chat unless a Hermes-native approval or\n question tool is visible in the current session.\n- Bootstrap host label: `host: \"Hermes\"`\n- Install/reload blocker label: Hermes install/reload problem\n- Reload instruction: restart Hermes, or run `/reload-mcp` in the active\n Hermes session after install\n\nDo not tell Hermes users to run Codex or Claude command forms, use Codex/Claude structured-question APIs, or restart Codex Desktop or Claude Code. Do not describe this run as Claude Code or Codex.\n\nHelp the customer keep the campaign they already have healthy and moving. This\nis one continuous workflow: understand current workspace state, choose one\nuseful next step, use the existing campaign owner, observe the result, and save\nthe exact wait or outcome.\n\n## Run the review\n\n1. Call `get_auth_status`, then obtain a fenced `customer_program` review claim\n and load the complete home-workspace campaign inventory. For a scheduled\n `execute` or `resume` wake, use the scheduled trigger and the wake gate's\n exact lease owner. For a direct customer request, use the interactive\n trigger. For a `reconcile` wake, skip campaign work and follow the delivery\n recovery below. Keep each strict tool input separate: claim with only\n `action`, `trigger`, and `leaseOwner`; never add the wake's `effectId` or\n `fence` to the claim call.\n2. Infer the relationship stage from that workspace state. Keep campaign health\n and wait state separate from the relationship stage.\n3. Assess each campaign from approved ICP, source, message and actual outcome\n evidence. `ACTIVE` or a full queue does not prove suitability. Sparse results\n do not prove poor quality. Unavailable data is not an empty workspace.\n4. Inspect supply and capacity, then choose exactly one outcome: `maintain`,\n `refill`, `continue`, `edit`, `propose_create`, `input`, or `blocker`.\n5. Execute through the existing owner:\n - suitable campaign with a real supply gap: call canonical Refill Sends with `yolo:true` and keep its returned run token. Keep going until the gap is filled or a real blocker remains; when a source runs dry, refill widens it. Do not stop to ask the customer for leads;\n - unfinished campaign: continue the same campaign through Create Campaign;\n - supported quality issue: edit the same campaign through Create Campaign;\n - no campaign: enter Create Campaign, asking only for the LinkedIn profile\n first when it is missing;\n - healthy supplied campaign: maintain it and stay quiet;\n - manual pause: respect it;\n - missing approval, connection or decision: with `launchConsent.mode: \"autopilot\"` approve messages that passed the quality check and start or resume the campaign yourself
|
|
48
|
+
"content": "---\nname: sellable-campaign-daily-review\ndescription: Review existing Sellable campaigns and move exactly one useful next step forward.\nvisibility: public\nallowed-tools:\n - mcp_sellable_get_auth_status\n - mcp_sellable_customer_program\n - mcp_sellable_get_campaigns\n - mcp_sellable_get_campaign\n - mcp_sellable_get_campaign_context\n - mcp_sellable_get_campaign_refill_state\n - mcp_sellable_refill_sends\n---\n\n# Sellable Campaign Daily Review\n\n## Installed Host Contract\n\nThis installed skill is running in Hermes Agent. When the shared workflow body\nor fallback text mentions Claude Code, Codex, or Hermes for internal parity,\nchoose the Hermes instruction for customer-facing language and host functions.\n\n- Customer-facing command: `/sellable-campaign-daily-review`\n- MCP tool naming: Hermes exposes Sellable tools as `mcp_sellable_<tool>`;\n when shared instructions show `mcp_sellable_<tool>`, call the matching\n `mcp_sellable_<tool>` tool instead.\n- Structured questions: ask plainly in chat unless a Hermes-native approval or\n question tool is visible in the current session.\n- Bootstrap host label: `host: \"Hermes\"`\n- Install/reload blocker label: Hermes install/reload problem\n- Reload instruction: restart Hermes, or run `/reload-mcp` in the active\n Hermes session after install\n\nDo not tell Hermes users to run Codex or Claude command forms, use Codex/Claude structured-question APIs, or restart Codex Desktop or Claude Code. Do not describe this run as Claude Code or Codex.\n\nHelp the customer keep the campaign they already have healthy and moving. This\nis one continuous workflow: understand current workspace state, choose one\nuseful next step, use the existing campaign owner, observe the result, and save\nthe exact wait or outcome.\n\n## Run the review\n\n1. Call `get_auth_status`, then obtain a fenced `customer_program` review claim\n and load the complete home-workspace campaign inventory. For a scheduled\n `execute` or `resume` wake, use the scheduled trigger and the wake gate's\n exact lease owner. For a direct customer request, use the interactive\n trigger. For a `reconcile` wake, skip campaign work and follow the delivery\n recovery below. Keep each strict tool input separate: claim with only\n `action`, `trigger`, and `leaseOwner`; never add the wake's `effectId` or\n `fence` to the claim call.\n2. Infer the relationship stage from that workspace state. Keep campaign health\n and wait state separate from the relationship stage.\n3. Assess each campaign from approved ICP, source, message and actual outcome\n evidence. `ACTIVE` or a full queue does not prove suitability. Sparse results\n do not prove poor quality. Unavailable data is not an empty workspace.\n4. Inspect supply and capacity, then choose exactly one outcome: `maintain`,\n `refill`, `continue`, `edit`, `propose_create`, `input`, or `blocker`.\n5. Execute through the existing owner:\n - suitable campaign with a real supply gap: call canonical Refill Sends with `yolo:true` and keep its returned run token. Keep going until the gap is filled or a real blocker remains; when a source runs dry, refill widens it. Do not stop to ask the customer for leads;\n - unfinished campaign: continue the same campaign through Create Campaign;\n - supported quality issue: edit the same campaign through Create Campaign;\n - no campaign: enter Create Campaign, asking only for the LinkedIn profile\n first when it is missing;\n - healthy supplied campaign: maintain it and stay quiet;\n - manual pause: respect it;\n - missing approval, connection or decision: with `launchConsent.mode: \"autopilot\"` approve messages that passed the quality check and start or resume the campaign yourself. When `launchConsent` has no answer yet, ask the one-time question instead of asking for approval: \"I can approve messages that pass my quality check and launch for you, then keep your sends filled. Reply review first if you would rather approve.\" Save the answer with `save_context` as Create Campaign's Standing Consent does. With `review_first`, retain one exact next step. Ask only for what you cannot do yourself, such as a LinkedIn connection, as one question.\n A built-in weekday refill job fills sends before this review; do not run\n Refill Sends again unless `refill` is this review's one outcome. Read\n `refill_v3_world_state` for the next two sender-local days: a row whose\n reason names a disconnected sender (`sender_disconnected`), a disconnected\n Sales Navigator (`sales_nav_disconnected`) or\n `paid_inmail_connection_health_recheck_required` is a fix only the customer\n can make. Make it this review's `input`: name the sender and say reconnect\n LinkedIn or reconnect Sales Navigator in Settings.\n6. Observe the owned operation, then record the outcome against the same review\n effect and fence. The `record_review_outcome` call contains only its action,\n effect, fence, review outcome, summary, and optional decision/operation refs;\n do not include trigger or lease fields. Never mark a pending or uncertain\n effect complete. On a scheduled actionable outcome, saving the outcome is\n not completion. Inspect `deliveryRequired`; when it is `true`, continue\n immediately to the required `stage_delivery` call. Never return while the recorded run still has `deliveryState: \"pending\"`. Write the outcome `summary` in plain customer words with the verified numbers (leads added, messages approved, sends scheduled): the next results update reads it as \"What Sellable did\".\n\n## Write the Slack message\n\nPost only new work or a real need, never a status. Use real line breaks: a bold title, a blank line between sections, one item per line, never one long paragraph. Write ordinary Markdown; delivery converts it for Slack.\n\n**{Workspace} · Campaign update · {Weekday Mon D}**\n\n**What I did** says in plain words what changed: leads added, messages approved,\nsends scheduled, campaign started, or a source widened.\n\n**Next step** and **Options** appear only when the customer must act: one specific\naction, then at most two options they can reply with. Offer only what you can really\ndo (refill, continue or edit the campaign) or what they can do (add leads, approve\nmessages, reconnect a sender).\n\nLead with what Sellable did to grow their sends. Never tell the customer what they\nhave not done, and never post a scoreboard of zeros. Plain words: no run tokens,\njob or effect IDs, error codes, tool names or file paths. Unknown is never zero.\nAbout 20-60 words.\n\n## Stop and delivery rules\n\nAn internal system fault (a run token, an error code, refill machinery) is not\na customer decision or blocker: do not post it.\n\nIf a tool call fails or its result can't be read, return `[SILENT]` and stop.\nNever post an error, an apology or a guess; the open claim lets the next\nscheduled window retry.\n\nStop on settled state, a human wait, a retained async wait, a typed blocker or\nthe existing bounded operation budget. Never restart a pending effect. Follow a\nnew customer reply or product completion immediately instead of waiting for the\nnext daily tick.\n\nPost a useful new decision or blocker once to the verified current main channel.\nRoutine refill belongs in the next results brief. A healthy no-op is inspectable\nand silent. On a scheduled actionable outcome, compose the complete final Slack\ntext, call `customer_program` with only `action: \"stage_delivery\"`, the exact\neffect and fence, and `text`, then inspect `delivery.deliverNow`. Only when it is `true`, return\n`delivery.text` verbatim as the native job's final response. When it is\n`false`, return `[SILENT]`; that effect is already uncertain or delivered,\nand the managed reconciliation tick owns the next step. Do not stage an interactive response.\n\nBefore any scheduled final response, check the recorded run one last time. A\n`pending` delivery is an unfinished cron turn: call `stage_delivery` now. Only\n`accepted_uncertain`, `delivered`, `failed`, or `not_applicable` may end the\nturn.\n\nOn a scheduled `reconcile` wake, call `reconcile_delivery` with only its action\nand the exact effect.\nIf trusted Slack history confirms the text, or retry is not yet allowed, return\n`[SILENT]`. If retry is allowed, call `retry_delivery` with the current fence and\nreturn its stored `delivery.text` verbatim. Never regenerate it. The native\nmanaged job remains the only Slack sender; the product owns the claim and\ndelivery ledger. After three unconfirmed attempts the product closes the run as\nfailed and later ticks stay silent. Never create another post path or fall back\nto another channel.\n\nDo not create a second campaign engine, refill algorithm, scheduler, scoring\nframework, intake questionnaire or generic morning approval. Do not rewrite\nstrategy or messages merely because another day passed. Initial campaign launch\nstill requires the existing human Start.\n"
|
|
49
49
|
},
|
|
50
50
|
{
|
|
51
51
|
"path": "skills/sellable/sellable-campaign-daily-results/SKILL.md",
|
package/package.json
CHANGED
|
@@ -44,12 +44,15 @@ the exact wait or outcome.
|
|
|
44
44
|
first when it is missing;
|
|
45
45
|
- healthy supplied campaign: maintain it and stay quiet;
|
|
46
46
|
- manual pause: respect it;
|
|
47
|
-
- missing approval, connection or decision: with `launchConsent.mode: "autopilot"` approve messages that passed the quality check and start or resume the campaign yourself
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
- missing approval, connection or decision: with `launchConsent.mode: "autopilot"` approve messages that passed the quality check and start or resume the campaign yourself. When `launchConsent` has no answer yet, ask the one-time question instead of asking for approval: "I can approve messages that pass my quality check and launch for you, then keep your sends filled. Reply review first if you would rather approve." Save the answer with `save_context` as Create Campaign's Standing Consent does. With `review_first`, retain one exact next step. Ask only for what you cannot do yourself, such as a LinkedIn connection, as one question.
|
|
48
|
+
A built-in weekday refill job fills sends before this review; do not run
|
|
49
|
+
Refill Sends again unless `refill` is this review's one outcome. Read
|
|
50
|
+
`refill_v3_world_state` for the next two sender-local days: a row whose
|
|
51
|
+
reason names a disconnected sender (`sender_disconnected`), a disconnected
|
|
52
|
+
Sales Navigator (`sales_nav_disconnected`) or
|
|
53
|
+
`paid_inmail_connection_health_recheck_required` is a fix only the customer
|
|
54
|
+
can make. Make it this review's `input`: name the sender and say reconnect
|
|
55
|
+
LinkedIn or reconnect Sales Navigator in Settings.
|
|
53
56
|
6. Observe the owned operation, then record the outcome against the same review
|
|
54
57
|
effect and fence. The `record_review_outcome` call contains only its action,
|
|
55
58
|
effect, fence, review outcome, summary, and optional decision/operation refs;
|