[object Object]

← back to Norma

IG cadence: 3 posts/day per account, spaced ~3.3h during business hours (was 1/day); batch 4->6

abf763c4b81ed221a89fc73556fbf782ed20fe2d · 2026-08-14 12:11:01 -0700 · Steve

Files touched

Diff

commit abf763c4b81ed221a89fc73556fbf782ed20fe2d
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri Aug 14 12:11:01 2026 -0700

    IG cadence: 3 posts/day per account, spaced ~3.3h during business hours (was 1/day); batch 4->6
---
 agents/instagram-agent/daily-cadence.js | 35 +++++++++++++++++++++++++--------
 1 file changed, 27 insertions(+), 8 deletions(-)

diff --git a/agents/instagram-agent/daily-cadence.js b/agents/instagram-agent/daily-cadence.js
index 827b1cb..0dd7ba7 100644
--- a/agents/instagram-agent/daily-cadence.js
+++ b/agents/instagram-agent/daily-cadence.js
@@ -1,6 +1,6 @@
 #!/usr/bin/env node
 /**
- * daily-cadence.js — "1 post/day per account, 6 days/week" driver.
+ * daily-cadence.js — "3 posts/day per account, spaced during business hours, 6 days/week" driver.
  *
  * Derives today's coverage from the post-ledger, picks the next batch of accounts
  * that still need today's post, auto-sources a guard-passing (no private-label leak)
@@ -35,10 +35,24 @@ const now = new Date();
 const isRestDay = now.getDay() === 0; // Sunday
 const today = now.toISOString().slice(0, 10);
 
-// posted today (from ledger)
+// Cadence target: 3 posts/day per account, spaced during business hours (Steve 2026-08-14).
+const TARGET_PER_DAY = parseInt(val('target', '3'), 10);
+const MIN_GAP_MS = 3.3 * 3600 * 1000; // ~3.3h between an account's posts → 3 fit in the 08:00–20:00 window
+
+// per-account today: how many posts + when the most recent one was (for the spacing gate)
 const led = path.join(HERE, 'data', 'post-ledger.jsonl');
 const rows = fs.existsSync(led) ? fs.readFileSync(led, 'utf8').split('\n').filter(Boolean).map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean) : [];
-const postedToday = new Set(rows.filter((r) => (r.ts || '').slice(0, 10) === today).map((r) => r.handle));
+const todayRows = rows.filter((r) => (r.ts || '').slice(0, 10) === today);
+const countToday = {}; const lastTs = {};
+for (const r of todayRows) { countToday[r.handle] = (countToday[r.handle] || 0) + 1; if (!lastTs[r.handle] || r.ts > lastTs[r.handle]) lastTs[r.handle] = r.ts; }
+const nowMs = now.getTime();
+// An account still needs a post if it's under today's target AND its last post was >= the gap ago
+// (last=0 when it hasn't posted today, so the gap check passes immediately).
+function needsPost(h) {
+  if ((countToday[h] || 0) >= TARGET_PER_DAY) return false;
+  const last = lastTs[h] ? Date.parse(lastTs[h]) : 0;
+  return (nowMs - last) >= MIN_GAP_MS;
+}
 const everPosted = new Set(rows.map((r) => r.product_handle)); // avoid reusing a product
 
 function themeFor(h) {
@@ -71,17 +85,21 @@ async function findProduct(keyword) {
   }
 
   const eligible = HANDLES.filter((h) => !themeFor(h).skip);
-  const remaining = eligible.filter((h) => !postedToday.has(h));
+  const remaining = eligible.filter((h) => needsPost(h)); // under target AND past the spacing gap
   const skipped = HANDLES.filter((h) => themeFor(h).skip).map((h) => `${h} (${themeFor(h).skip})`);
 
   if (flag('status')) {
-    console.log(`COVERAGE ${today}: ${postedToday.size}/${eligible.length} eligible accounts posted; ${remaining.length} remaining.`);
+    const totalSlots = eligible.length * TARGET_PER_DAY;
+    const doneSlots = eligible.reduce((a, h) => a + Math.min(countToday[h] || 0, TARGET_PER_DAY), 0);
+    const waiting = eligible.filter((h) => (countToday[h] || 0) < TARGET_PER_DAY && !needsPost(h)).length;
+    console.log(`COVERAGE ${today}: ${doneSlots}/${totalSlots} post-slots filled (target ${TARGET_PER_DAY}/account across ${eligible.length} accounts).`);
+    console.log(`Eligible right now: ${remaining.length} · Waiting on spacing gap (~3.3h): ${waiting}`);
     console.log(`Excluded: ${skipped.join(', ') || 'none'}`);
-    console.log(`Remaining: ${remaining.join(', ') || '(day complete)'}`);
+    console.log(`Eligible now: ${remaining.join(', ') || '(none — all at target or within gap)'}`);
     return;
   }
 
-  if (!remaining.length) { console.log(`DAY COMPLETE — all ${eligible.length} eligible accounts posted for ${today}.`); return; }
+  if (!remaining.length) { console.log(`No accounts eligible this run for ${today} (all at ${TARGET_PER_DAY}/day target or within the ~3.3h spacing gap).`); return; }
 
   // next batch, sensitive accounts deferred to Cody (reported, not auto-posted)
   const batch = [];
@@ -95,7 +113,8 @@ async function findProduct(keyword) {
     batch.push({ account: h, product: prod.handle, title: prod.title });
   }
 
-  console.log(`Daily cadence ${today} — ${postedToday.size}/${eligible.length} done, posting next ${batch.length}${DRY ? ' (DRY)' : ''}:`);
+  const filled = eligible.reduce((a, h) => a + Math.min(countToday[h] || 0, TARGET_PER_DAY), 0);
+  console.log(`Cadence ${today} — ${filled}/${eligible.length * TARGET_PER_DAY} slots filled (${TARGET_PER_DAY}/acct), posting next ${batch.length}${DRY ? ' (DRY)' : ''}:`);
   const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
   for (let i = 0; i < batch.length; i++) {
     const b = batch[i];

← 41aaea0 IG cadence: exclude @goldleafwallpaper (owned by dedicated R  ·  back to Norma  ·  IG: #fabricfriday reshare tool (hashtag-search picks + sassy 5dec32d →