diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 3be772b..9ac40ec 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -60,9 +60,9 @@ # DEPLOY_KEEP_RELEASES old releases to keep on disk (default 4) # DEPLOY_KEEP_BACKUPS pre-swap DB/pm2 backups to keep (default 10) # DEPLOY_QUIET_HOURS_KST hours (KST) in which to defer the deploy entirely -# (default "6 7 8 9 12 13" — the cron slots; a swap -# re-registers every cron and pm2 runs each once). -# Set to an empty string to disable. +# (default "6 7 8 13" — cron slots whose script is not +# yet safe to re-run; a swap re-registers every cron +# and pm2 runs each once). Empty string disables. # DEPLOY_HOLD_FILE operator hold — exists ⇒ do nothing # DEPLOY_CI_WAIT_MIN how long to keep waiting for a CI verdict (run not # created yet, or GitHub API failing) before deploying @@ -439,7 +439,14 @@ main() { # `-` not `:-`: unset takes the default, but an explicit empty string means # "no quiet hours". With `:-` the empty value was silently replaced by the # default and the guard could not be turned off. - DEPLOY_QUIET_HOURS_KST=${DEPLOY_QUIET_HOURS_KST-"6 7 8 9 12 13"} + # 09 (persona-tick) and 12 (persona-replies) came off the list on 2026-08-20: + # both scripts now count what the KST day already has and write only the + # difference, so the immediate re-run a swap triggers is a no-op. The hours + # still listed are the ones whose crons have not been verified re-run-safe + # the same way — 06 macro, 07 synthesis/seed-qa/connections, 08 brief/ + # translate/why-moved, 13 calls. Each can be dropped once its script carries + # the same guard. + DEPLOY_QUIET_HOURS_KST=${DEPLOY_QUIET_HOURS_KST-"6 7 8 13"} # The webhook is a credential and this repo is public, so it lives only in # a server-side .env.local (gitignored). The TS scripts read the LIVE # RELEASE's copy; that is preferred once LIVE_DIR is known (below, in diff --git a/scripts/persona-replies.ts b/scripts/persona-replies.ts index 8ad280a..998c564 100644 --- a/scripts/persona-replies.ts +++ b/scripts/persona-replies.ts @@ -78,6 +78,25 @@ async function main() { const { getDb } = await import("../lib/db"); const db = getDb(); + // Same day-level guard as scripts/persona-tick.ts: a deploy swap re-runs + // every cron immediately, and the per-persona daily cap (agent.dailyCap / 2) + // leaves room for a second round. Count what today already has and write + // only the difference — which also makes a crashed run resumable. + const repliedToday = ( + db + .prepare( + `SELECT COUNT(*) AS n FROM alpha_posts + WHERE author_kind = 'agent' AND parent_id IS NOT NULL AND is_deleted = 0 + AND datetime(created_at, '+9 hours') >= date('now', '+9 hours')` + ) + .get() as { n: number } + ).n; + if (repliedToday >= max) { + console.log(`Already replied ${repliedToday}/${max} today — nothing to do.`); + return; + } + const remaining = max - repliedToday; + // 최근 24시간 페르소나 글 (답글 < 5) // // Agent-authored parents only, unless explicitly opted into. @@ -118,13 +137,15 @@ async function main() { const agents = getActiveAgents(); - console.log(`Reply candidates: ${candidates.length}, max ${max} replies`); + console.log( + `Reply candidates: ${candidates.length}, target ${remaining}${repliedToday ? ` (${repliedToday} already today, cap ${max})` : ""} replies` + ); let posted = 0; let totalCost = 0; let attempts = 0; for (const c of candidates) { - if (posted >= max) break; + if (posted >= remaining) break; attempts++; // Pick replier: stance-contrast preferred diff --git a/scripts/persona-tick.ts b/scripts/persona-tick.ts index 5a7390c..4a084f3 100644 --- a/scripts/persona-tick.ts +++ b/scripts/persona-tick.ts @@ -97,6 +97,34 @@ async function main() { const agents = getActiveAgents(); const today = new Date().toISOString().slice(0, 10); + // Already done today? Then this run has nothing to add. + // + // A deploy swap re-registers every cron app and pm2 runs each one once, + // immediately. Every other cron survives that — brief is keyed by date, + // why-moved compares pulse sets, calls skips posts that already have one — + // but this one had no such guard, so a swap inside the 09:00 KST hour + // published a second full round of ten. That is the whole reason + // DEPLOY_QUIET_HOURS_KST blocks 09 and 12 (scripts/deploy.sh), which costs + // up to two hours of deploy latency every day. + // + // Counting what today already has also makes a partial run resumable: if the + // process died after four posts, the next invocation writes six, not ten. + const { getDb } = await import("../lib/db"); + const postedToday = ( + getDb() + .prepare( + `SELECT COUNT(*) AS n FROM alpha_posts + WHERE author_kind = 'agent' AND parent_id IS NULL AND is_deleted = 0 + AND datetime(created_at, '+9 hours') >= date('now', '+9 hours')` + ) + .get() as { n: number } + ).n; + if (postedToday >= pages) { + console.log(`Tick ${today}: already posted ${postedToday}/${pages} today — nothing to do.`); + return; + } + const remaining = pages - postedToday; + // Build candidate pool — entity/topic/event with videoCount ≥ 3 type Candidate = { refType: "entity" | "topic" | "event" | "asset"; refId: string }; const pool: Candidate[] = []; @@ -159,16 +187,16 @@ async function main() { } console.log( - `Tick ${today}: pool=${pool.length} (persons skipped ${skippedPersons}, priceable reserved ${reserved.length}), types=${[...selectedTypes].join(",")}, agents=${agents.length}, target=${pages} posts` + `Tick ${today}: pool=${pool.length} (persons skipped ${skippedPersons}, priceable reserved ${reserved.length}), types=${[...selectedTypes].join(",")}, agents=${agents.length}, target=${remaining}${postedToday ? ` (${postedToday} already today, cap ${pages})` : ""} posts` ); let posted = 0; let totalCost = 0; let attempts = 0; - const maxAttempts = pages * 5; // safety bound + const maxAttempts = remaining * 5; // safety bound for (const c of pool) { - if (posted >= pages || attempts >= maxAttempts) break; + if (posted >= remaining || attempts >= maxAttempts) break; attempts++; // Pick an agent, prefer those that haven't posted recently @@ -195,7 +223,7 @@ async function main() { } console.log( - `\nTick done. Posted: ${posted}/${pages} · cost: $${totalCost.toFixed(4)} · attempts: ${attempts}` + `\nTick done. Posted: ${posted}/${remaining} · cost: $${totalCost.toFixed(4)} · attempts: ${attempts}` ); }