Blast Scheduler Discovery — Q1 Resolution
Date: 2026-04-10 Reference: Plan Section 66 Step 5, Section 50, Q1 open question Status: RESOLVED
Question
Find every code path that INSERTs into dispo_blast_recipients, and determine whether each path goes through scripts/lib/salesmsg-send.js (the intended chokepoint) or has its own send mechanism.
Method
grep -rn "dispo_blast_recipients" /home/opsadmin/.openclaw/workspace \
--include="*.js" --include="*.cjs" --include="*.ts" \
| grep -iE "insert|upsert"Then for each unique file: check the sendSms definition and import paths.
Findings — All write paths
| # | Script | Write Sites (line numbers) | Has private sendSms? | Wraps lib/salesmsg-send? | Runtime |
|---|---|---|---|---|---|
| 1 | scripts/dispo-blast-engine.js | 868, 887, 900, 1033, 1046, 1057 | YES (line 410, fully independent) | NO | manual CLI |
| 2 | scripts/dispo-propstream-blast.js | 1086, 1286 | YES (line 1072, wrapper) | YES (line 1074) | manual CLI |
| 3 | scripts/dispo-investorbase-blast.js | 561, 762 | YES (line 509, wrapper) | YES (line 511) | manual CLI |
| 4 | scripts/dispo-crmls-agent-blast.js | 1192, 1344 | YES (line 1178, wrapper) | YES (line 1180) | manual CLI |
| 5 | scripts/lovable-api-server.js | 3365 (insert), 3545 (upsert) | NO — direct import | YES (line 3229) | RUNNING (ops-dashboard) |
Total: 5 distinct scripts, 13 distinct insert/upsert sites.
Critical findings
Finding 1 — dispo-blast-engine.js is the ONE script that bypasses the chokepoint
It defines its own sendSms() function at line 410 that does NOT call lib/salesmsg-send.js. Any rate limiter, opt-out check, cooldown, or HTML strip added to the chokepoint will NOT catch this script.
Implication for Phase 0:
The send-layer chokepoint enforcement (Actions 1, 2, 3, 5, 5b, 5c) catches 4 of 5 blast scripts and the live webhook handler, pgmq consumer, and lovable-api-server. dispo-blast-engine.js requires a separate refactor to either:
- (a) Refactor to import
lib/salesmsg-send.jslike the other 3 wrapper scripts do (~1 hour of work) - (b) Add the same gates inline to its private
sendSms()(duplicates code, not recommended)
Recommended fix: Option (a). Refactor dispo-blast-engine.js to use the chokepoint. This becomes a new step in Section 66 build order.
Finding 2 — None of the blast scripts run on a recurring schedule
No PM2 entries, no systemd timers, no crontab entries trigger any of the blast scripts. They are all manual CLI invocations OR triggered by API calls into lovable-api-server.js.
This is a meaningful threat-model insight. The “20,354 sends in 7 days” volume from Section 27.10.1 came from human-initiated blasts, not from a runaway autonomous loop. Phase 0 send-layer rate limits will gate every send at the chokepoint, but the gating only matters when humans actually run the scripts. There’s no autonomous fire-hose to stop.
Finding 3 — dispo-blast-engine.js exports its private sendSms
Line 1174: module.exports = { sendSms, ... }. Searched for any importer:
grep -rn "require.*dispo-blast-engine" /home/opsadmin/.openclaw/workspaceResult: only one match, and it’s COMMENTED OUT in webhooks/hubspot-handler.js line 260. No live consumer of the engine’s exported sendSms. Safe to refactor.
Finding 4 — lovable-api-server.js IS running and uses the chokepoint correctly
Lines 3229, 3358, 3780, 3783 all call lib/salesmsg-send.js. This is good — Phase 0 enforcement will catch it automatically.
But also: lovable-api-server.js is a 8,645-line monolith and hosts the web UI at ops-dashboard.service. It writes to dispo_blast_recipients directly via lines 3365 and 3545. The Phase 0 enforcement catches the SEND but does NOT catch the database insert. If a rate-limited send is blocked at the chokepoint, the recipient row would still be inserted with no sent_at timestamp — leaving orphan rows. Need to handle this case in the chokepoint: if a send is blocked, do NOT also leave a blast row without a sent_at.
Phase 0 implications
- Add Step 13.5 to the build order: Refactor
dispo-blast-engine.jsto uselib/salesmsg-send.js. ~1 hour. Can ship in parallel with other Phase 0 work. - Phase 0 chokepoint behavior must NOT leave orphan
dispo_blast_recipientsrows when a send is blocked. Either return the block reason to the caller so they can clean up, OR have the chokepoint emit apipeline_eventsevent that a sweep cron can use to clean orphans later. - No urgent need for autonomous-loop circuit breaker at the blast-engine level — these scripts are human-triggered. The per-phone cooldown at the chokepoint is sufficient for the rate-limiting use case.
Phase 1 implications (rate limiter)
Section 50 (Blast Scheduler Rate Limiting Architecture) was marked PROVISIONAL pending Q1. With Q1 resolved:
- Primary enforcement: Add the rate limiter logic inside
lib/salesmsg-send.js(single source). Every send through the chokepoint gets the per-phone-per-24h, per-phone-per-7d, and per-deal-per-day caps applied automatically. - Defense in depth: After dispo-blast-engine refactor (Step 13.5), all 5 blast scripts go through the chokepoint and get the rate limit. No need for a separate scheduler-side enforcement layer.
- Exception handling: Override mechanism via
opts.override_reasonparameter onsendSms(). Logged topipeline_eventswith the override reason for audit.
This SIMPLIFIES the Section 50 architecture significantly. The “blast scheduler is in Airtable automations” branch can be eliminated — there are no Airtable-driven blasts.
Action Items
| # | Action | Owner | Phase | Status |
|---|---|---|---|---|
| 1 | Refactor dispo-blast-engine.js to use lib/salesmsg-send.js | Engineering | 0 (insert as Step 13.5) | NEW |
| 2 | Verify Phase 0 chokepoint cleans up orphan dispo_blast_recipients rows on send block | Engineering | 0 | NEW |
| 3 | Update Section 50 to remove the “Airtable automation” provisional branch | Plan author | next plan append | NEW |
| 4 | Run `git blame /home/opsadmin/.openclaw/workspace/scripts/dispo-blast-engine.js | sed -n ‘410p’` to determine when private sendSms was added | Engineering | research |
Files inspected
/home/opsadmin/.openclaw/workspace/scripts/dispo-blast-engine.js/home/opsadmin/.openclaw/workspace/scripts/dispo-propstream-blast.js/home/opsadmin/.openclaw/workspace/scripts/dispo-investorbase-blast.js/home/opsadmin/.openclaw/workspace/scripts/dispo-crmls-agent-blast.js/home/opsadmin/.openclaw/workspace/scripts/lovable-api-server.js/home/opsadmin/.openclaw/workspace/scripts/workers/dispo-approval-poller.js/home/opsadmin/.openclaw/workspace/scripts/crons/dispo-morning-recovery.js/home/opsadmin/.openclaw/workspace/webhooks/hubspot-handler.js(only commented reference)
Conclusion
Q1 RESOLVED. The blast scheduler is NOT an Airtable automation. It is 5 Node.js scripts in workspace/scripts/, all manually invoked or web-triggered. 4 of 5 already wrap lib/salesmsg-send.js. The 1 outlier (dispo-blast-engine.js) needs a small refactor to align with the chokepoint pattern. After that refactor, Phase 0 send-layer enforcement covers 100% of dispo outbound traffic and the rate limiter (Section 50) can ship as a single change to the chokepoint without needing scheduler-side enforcement.