Every release, every meaningful change. Updated when something ships, not when marketing remembers to mention it.
v3.7.14
Cross-poster dedup at lead-insert + swarm temperature locked to 0.2
addedCross-poster dedup runs at lead-insert. When the same author posts a normalized-title-equivalent piece across multiple stations within 72h, the duplicate candidate is dropped before the swarm fires. Kills 3× LLM waste, 3× operator review burden, and 3× RL penalty distribution per cross-post. Emits a cross_post_dropped event referencing the original lead so audits of "where did that mission go?" land at the right answer. Knob: CROSS_POST_DEDUP_HOURS (default 72).
fixedSwarm classifier temperature now set to 0.2 across all three provider paths (Anthropic / OpenAI / Gemini). Pre-fix none of them set temperature, so all ran on provider defaults (1.0), injecting ±5–10 point score variance into the 40/35/25 weight fusion. Does not affect draft generation (still 0.7 / 1 for natural-sounding outreach copy) and does not affect prompt caching (cache key is prompt content, not sampling params).
v3.7.13
Scout cron extracted from the schema file + cadence corrected to 10 minutes
changedThe pg_cron heartbeat that calls /scout/launch_batch lives in Supabase, not Railway. v3.7.13 extracted the cron.schedule() block out of supabase_schema.sql (which had been shipping with YOUR-RAILWAY-URL / YOUR-OPERATOR-KEY placeholders — a footgun on fresh-DB setups) into a separate _cron_setup.sql deployment artifact. Schema file now declares only the pg_cron / pg_net extensions; the actual schedule with real values is a one-time-per-environment ops step.
fixedCadence corrected from `0 */2 * * *` (every 2h — an operator testing artifact) to `*/10 * * * *` (every 10 min — design cadence). 12× more cron fires per day. The 2h gap had been producing many feed-dry cycles where buying signals expired before the next scout, masquerading as a calibration regression.
fixed_cron_setup.sql uses `select cron.unschedule(jobid) from cron.job where jobname = X` instead of `delete from cron.job where jobname = X` — the latter fails on Supabase with ERROR 42501 (the cron schema is privileged; cron.unschedule() is SECURITY DEFINER so it bypasses table-level RBAC). Idempotent for both first installs and rotations.
fixedMCP _project_mission() was reading a phantom missions.score column that never existed. Now projects leads.signal_score with missions.raw_score as fallback. Uses `is not None` (not `or`) so legitimate score=0 doesn't fall through. Fixes the case where every mission appeared identically calibrated in the MCP listing because the scoring field was always null.
changedEducator-role Skeptic prompt rewritten with a four-clause direction veto: (a) how-to-build, (b) generic startup / first-customer advice without acquisition pain in an existing operation, (c) competing-tool author even when framed as a question, (d) entirely off-topic. Catches the failure mode where on-topic-but-wrong-intent posts (e.g. "how to get my first paying customers" or "I'm building a lead generator, advice?") were clearing the gate at score ≥ 0.45.
changed_SELLER_VETO extended from a single clause to three: (1) seller / promo / announcement, (2) builder / maker / pre-launch, (3) operating competitor. Distinguishes BUY vs BUILD intent, preserves the buyer counter-example so legitimate buyer-pain venting still scores correctly.
addedScore-aware channel downgrade: if a candidate mission would have used cold-DM channel (reddit_dm, twitter_dm) AND role == "educator" OR raw_score < 60, downgrade to channel = "manual" so the operator reviews before any DM lands. Public replies (twitter_reply) keep their platform mapping; warm leads (closer / advisor or raw_score ≥ 60) flow normally.
addedStartup warning when an active product has zero buy_signal_keywords loaded. Catches the silent self-disable failure mode where the cheap keyword pre-filter is empty and the 0.15 embedding floor becomes the only filter.
v3.7.11
Sidecar ack now writes prospects.last_signal + schema file resynced to prod
fixedThe /actions/ack=success path now writes prospects.last_signal = "message_sent" + last_contact_at = now() (mirrors the companion_track / record_reply pattern). Closes the last leg of the v3.7.10 audit-trail repair: outbound interactions, prospect lifecycle, and dashboard signal all stay in sync regardless of which write path produced the send.
changedsupabase_schema.sql fully rewritten against information_schema. Was frozen at v3.5, missing ~15 columns and 4 tables (events, send_windows, public_stats, product_anchor_history). The schema file is now a reliable grep target for future migration authors — the upstream cause of the v3.7.10 backfill scope bug was that nobody could trust the file.
v3.7.10
Sidecar /actions/ack audit-trail gap closed (plus a same-day backfill correction)
fixedWhen the sidecar ack'd a mission as sent, no interaction row was written. Dashboard, analytics, and the RL loop all read interactions as the canonical send record, so a successful send produced zero downstream signal. Added _record_outbound_message() called from the success branch of /actions/ack.
fixedSame-day backfill correction shipped as v3.7.10_backfill_correction.sql (commit f892d72). The original backfill swept missions.status="sent" but ignored missions.outreach_channel, producing 3 false positives where channel="manual" missions (operator-completed via the native platform UI) were back-filled as automated sends. Correction drops those 3; future backfills must filter on outreach_channel.
fixedcompanion_track() and record_reply() were both reading prospect["last_signal"] via .get() — but the column did not exist on the prospects table. Both .get() calls silently returned None, breaking the mode-transition rule that depends on the latest named signal. v3.7.9 adds the column, patches both write sites (companion_track, record_reply) to populate it, and aligns the read sites accordingly. The fix arrived together with v3.7.10 because the two bugs were structurally siblings — both manifestations of "schema drifted; reads assumed a column that wasn't there".
v3.7.8
Mode transitions are now intent-based — first-touch prospects stay in Nurture
fixedThe prospect mode-transition rule used to flip any prospect at temperature < 30 into Recovery on their first interaction, because the default starting temperature is 10. That meant brand-new contacts who had received exactly one DM — first-touch, no reply yet — got the Re-engager persona ("I know it's been a while…") on day-2 follow-ups, despite never having engaged. Now Nurture prospects only move into Recovery on an explicit cooling signal (ghosted_3_days, ghosted_7_days, not_interested, bad_timing) or after they've actually engaged (sales/closing) and then cooled. Fresh prospects stay in Nurture and get the Educator voice — value-first, no hard sell — until they actually engage or actually ghost.
changedPublic temperature-model docs updated across the guide, README, llms-full.txt, OpenAPI surface, and MCP get_pipeline tool description. The model is now described as intent + temperature, not pure temperature. Nurture mode is documented as the default first-touch state for the first time.
addedv3.7.8_migration.sql resets prospects currently stuck in Recovery without any engagement or cooling signal history — these were misclassified by the old rule and the fix moves them back to Nurture so the next outreach picks the correct persona.
v3.7.7
Per-station reinforcement learning — feed-level demotion replaces global product weight
changedRL weight is now per-station, not per-product. Each listening feed (subreddit, HN search, RSS source) carries its own rl_weight (default 1.0, clamped 0.5–2.0) that multiplies the raw signal score before the 50-point threshold. So a single noisy feed gets demoted without dragging the rest of the product's stations down — fixing the issue where SignalPipe's product-level weight had drifted to 0.51, suppressing good leads from good stations alongside bad ones.
addedleads.station_id (nullable FK to stations) so the reject path can resolve mission → lead → station and apply the RL delta to the correct feed. Old leads keep station_id = NULL (no reliable reverse-resolution from a 6-week-old URL); legacy leads still record rejection_stats but don't move any rl_weight.
changedproducts.rl_weight column reset to 1.0 for all products. It still exists for backward compatibility and rejection_stats analytics still flow through it, but it is no longer used as a scoring multiplier.
fixedScout loop and /actions/reject now both write to stations.rl_weight; /feedback/record fallback prefers the station weight when available. 8 new unit tests cover the resolve → update → return paths.
v3.7.6
Swarm gate tightened to 0.45 + auto-delete low-confidence missions
changedSwarm fused-confidence gate raised from 0.40 to 0.45. Below the new gate, the sidecar no longer uploads a placeholder draft — the mission row is auto-deleted (silent cleanup, no learning signal). Belt-and-suspenders with the v3.7.5 station deactivation: gate the missions AND stop polling the worst feeds.
changedDeactivated 5 additional noise stations identified in the 2026-05-17 queue audit (r/VintageStory, r/EmailMarketingIndia, r/ObsidianMD, r/VirtualAssistant, u_Independent-Tap-323) — communities with zero buyer signal for SignalPipe.
v3.7.5
Noise station purge + idempotent approve RL
changedDeactivated 7 noise stations that produced almost all the queue noise (r/jobhuntify, r/WhiteLabelSEOAgencies, r/AISEOInsider, r/RampPlatform, r/openclaw, plus two single-user brand-promo feeds). Soft-deleted via active=false so per-station history stays queryable and re-enabling is one UPDATE away.
fixed/actions/approve is now idempotent — replaying an approval no longer double-applies the RL weight bump. Critical for sidecar retries.
v3.7.4
Universal score_signal + value_prop covers all channels
addedsignalpipe_score_signal — universal text classifier callable from any MCP client. Scores arbitrary text from any channel the host agent reads (Gmail, Slack, Discord, Telegram, LinkedIn, TikTok, Instagram, support tickets, transcripts) against any configured product profile. Same engine the scout uses on Reddit/HN posts; SignalPipe never touches the source platform — the user's agent does.
changedSignalPipe product row's value_prop and target_audience rewritten so the 9-prompt swarm knows the system also covers TikTok, Instagram, Slack, Gmail, etc. via score_signal. Fixes the case where the educator persona recommended competitor monitoring tools to a TikTok/Instagram question because its own product description only mentioned Reddit/HN.
changedEducator persona prompts hardened with no-competitor-recommendation directive (_NO_COMPETITOR_REC).
fixedMCP /mcp endpoint returned 421 "Invalid Host header" on every authenticated request — caused by the MCP Python SDK's DNS rebinding protection rejecting any Host other than localhost. Fixed by configuring TransportSecuritySettings with the public + Railway hostnames whitelisted.
fixedBehind Railway's TLS-terminating edge, FastAPI emitted 307 redirects with Location: http:// (HTTPS→HTTP downgrade), causing MCP clients to strip the Authorization header and fall back to OAuth. Added uvicorn --proxy-headers --forwarded-allow-ips=* so X-Forwarded-Proto is honored.
fixed/oauth/token rejected base64url codes with stripped padding (the standard OAuth client encoding). Re-pad before decoding so both padded and unpadded codes work.
fixedreject_mission silently broken — the rejection_reason column was referenced in code but never added to the missions table, so every reject returned PostgREST PGRST204. RL learning quality degrades without per-reason penalties; v3.7.3 migration adds the column + NOTIFY pgrst.
changedMCP response contract: get_missions defaults include_context=False (verbose context is opt-in via draft_mission), all read-tool projections cap user-content strings server-side (snippet 220, draft 500, value_prop 200, last_signal 80).
changedMCP tool docstrings rewritten as presentation guidance for the host LLM — explicit when-to-use vs companion tools (e.g. reject vs delete), full reason taxonomy with penalty intent, no-shell-introspection directives.
v3.7.2
Swarm low-confidence flag for the operator queue
addedmissions.swarm_low_confidence boolean — set TRUE when the swarm fuses below the 0.40 gate and the sidecar uploads a placeholder draft. Lets the dashboard collapse low-confidence rows behind an expander instead of dead-lettering them.
addedMCP get_missions projection exposes swarm_low_confidence so clients can filter the noise tier.
v3.7.1
Educator skeptic softened, regression test repaired
changedEducator-tier skeptic now only vetoes leads that are entirely off-topic — was previously vetoing borderline-tier signals that the analyst should have been allowed to judge.
fixedRegression test for the 9-prompt swarm matrix updated to track the new educator cues.
Client-side drafting + remote MCP server + operator console
addedRemote MCP server at api.signalpipe.io/mcp exposing 15 tools (missions: 6 · pipeline: 4 · products: 4 · scout: 1) for Claude Code, Cursor, and Windsurf.
addedRole-aware swarm — closer / advisor / educator role assigned per lead, swaps the system prompt for each of the 3 judges. 9-prompt matrix in total.
changedRole assignment now uses content_score (pre-floor truth) instead of signal_score, so a misclassified competitor mention can't dishonestly promote a weak post to "closer" voice.
changedCron cadence reconciled to 2 hours across schema and README. Feed-fetch limit raised to 50 entries per station.
fixedCompetitor floor was hijacking misclassified leads — now scoped to genuine signal.
fixedReddit RSS user-agent now identifies as a polite bot to avoid 429 blocks.
v3.6
Correctness, hardening, lethality
changedTightened scoring math edge cases — geometric mean now stable for zero-component leads.
changedCORS scoped to signalpipe.io / www.signalpipe.io for dashboard requests.
fixedSidecar idle poll now logs an explicit "queue empty" message so Railway logs reflect liveness when missions are absent.
v3.5
Batch embeddings + multi-factor signal
addedFreshness, engagement, and author-reputation multipliers in the signal score.
addedReply capture — outcomes feed back into the RL loop.
addedSwarm disagreement metric exposed on missions.
changedEmbedding calls batched per scout cycle — significant cost reduction on busy stations.
v3.0
Multi-factor scoring + adaptive RL
addedMulti-factor weighted geometric mean replaces single-component score.
addedPer-product rl_weight (0.5–2.0) tuned by approve / reject feedback.
addedProspect temperature momentum and time decay for the nurture engine.
v2.0
Mission control + interactions ledger
addedMissions table — every scored lead becomes a reviewable mission with draft, status, and outcome.
addedAnti-spam ledger — interactions table prevents the same URL being processed twice per product.
addedAuto-approve threshold (configurable, off by default).
For the technical surfaces these versions changed, see the API reference or the glossary.