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

#ScriptWrite Sites (line numbers)Has private sendSms?Wraps lib/salesmsg-send?Runtime
1scripts/dispo-blast-engine.js868, 887, 900, 1033, 1046, 1057YES (line 410, fully independent)NOmanual CLI
2scripts/dispo-propstream-blast.js1086, 1286YES (line 1072, wrapper)YES (line 1074)manual CLI
3scripts/dispo-investorbase-blast.js561, 762YES (line 509, wrapper)YES (line 511)manual CLI
4scripts/dispo-crmls-agent-blast.js1192, 1344YES (line 1178, wrapper)YES (line 1180)manual CLI
5scripts/lovable-api-server.js3365 (insert), 3545 (upsert)NO — direct importYES (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.js like 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/workspace

Result: 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

  1. Add Step 13.5 to the build order: Refactor dispo-blast-engine.js to use lib/salesmsg-send.js. ~1 hour. Can ship in parallel with other Phase 0 work.
  2. Phase 0 chokepoint behavior must NOT leave orphan dispo_blast_recipients rows when a send is blocked. Either return the block reason to the caller so they can clean up, OR have the chokepoint emit a pipeline_events event that a sweep cron can use to clean orphans later.
  3. 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_reason parameter on sendSms(). Logged to pipeline_events with 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

#ActionOwnerPhaseStatus
1Refactor dispo-blast-engine.js to use lib/salesmsg-send.jsEngineering0 (insert as Step 13.5)NEW
2Verify Phase 0 chokepoint cleans up orphan dispo_blast_recipients rows on send blockEngineering0NEW
3Update Section 50 to remove the “Airtable automation” provisional branchPlan authornext plan appendNEW
4Run `git blame /home/opsadmin/.openclaw/workspace/scripts/dispo-blast-engine.jssed -n ‘410p’` to determine when private sendSms was addedEngineeringresearch

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.