Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions scripts/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
25 changes: 23 additions & 2 deletions scripts/persona-replies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
36 changes: 32 additions & 4 deletions scripts/persona-tick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand Down Expand Up @@ -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
Expand All @@ -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}`
);
}

Expand Down
Loading