Add inactive user re-engagement rollout script#29
Add inactive user re-engagement rollout script#29marius-kilocode wants to merge 7 commits intomainfrom
Conversation
Register 'cli-v1-rollout' promo credit category ($1, 7-day expiry, idempotent) and add a script to grant credits to lapsed users who haven't used Kilo in 30+ days.
| const recentlyActiveRows = await db | ||
| .selectDistinct({ userId: microdollar_usage.kilo_user_id }) | ||
| .from(microdollar_usage) | ||
| .where(gte(microdollar_usage.created_at, cutoff.toISOString())); |
There was a problem hiding this comment.
WARNING: Possible type mismatch in gte() comparison (created_at vs ISO string)
microdollar_usage.created_at is likely a timestamp/date column, but the query compares it to cutoff.toISOString() (a string). Depending on the Drizzle/driver typing and the DB, this can lead to incorrect filtering (e.g., active users not being excluded) or runtime/type issues. Prefer passing a Date (e.g., cutoff) so the driver performs the correct parameter binding.
There was a problem hiding this comment.
don't know about this one, please test/try
| console.log('\nThis was a DRY RUN. No actual changes were made.'); | ||
| console.log('To apply changes, run with --apply flag'); | ||
| } else { | ||
| console.log(`\nTotal credits granted: $${stats.successful.toFixed(2)}`); |
There was a problem hiding this comment.
SUGGESTION: Misleading ‘Total credits granted’ log uses user count
stats.successful tracks number of users granted, not USD. The current log prints e.g. $1234.00 for 1234 users. If the intent is USD, multiply by the per-user amount.
| console.log(`\nTotal credits granted: $${stats.successful.toFixed(2)}`); | |
| console.log(`\nTotal credits granted: $${(stats.successful * 1).toFixed(2)}`); |
Code Review SummaryStatus: 2 Issues Found | Recommendation: Address before merge Overview
Fix these issues in Kilo Cloud Issue Details (click to expand)WARNING
SUGGESTION
Files Reviewed (5 files)
|
| console.log(` Failed: ${stats.failed}`); | ||
| console.log(` Rate: ${usersPerSecond.toFixed(2)} users/sec`); | ||
| console.log(` Last user ID: ${lastUserId}`); | ||
| console.log(` To resume from this point: --resume=${lastUserId}`); |
There was a problem hiding this comment.
WARNING: Resume hint can skip unprocessed users
lastUserId is set to the last ID in the fetched page (users[users.length - 1].id) before processing begins, but the progress log prints --resume=${lastUserId} as if it were the last processed user. If the script crashes mid-page, resuming from that value will restart after the end of the page, skipping any users in that page that were not processed yet. Consider tracking and printing a lastProcessedUserId (updated after each user completes) and using that for the resume cursor.
| // Resume support: If script crashes, you can resume from the last processed user ID | ||
| // Example: pnpm script src/scripts/d2025-02-04_cli-v1-rollout.ts --resume=user-abc-123 | ||
| const resumeFromArg = process.argv.find(arg => arg.startsWith('--resume=')); | ||
| const RESUME_FROM_USER_ID = resumeFromArg ? resumeFromArg.split('=')[1] : null; |
There was a problem hiding this comment.
I would instead just let it only process users who dont have credits yet, this seems like nonsense
There was a problem hiding this comment.
This was just copied from the previous rollout script
There was a problem hiding this comment.
I'm not saying the previous script wasnt vibed :P
markijbema
left a comment
There was a problem hiding this comment.
I think i would make the script get the X recently active rows without these credits, and then give them all credits (maybe do a few batches, but not as small as opus thinks)
Then you have a bit more control, and it's easier to resume;
Looking at who we are trying to target, I'd even say it would be better in two phases; first add them to a cohort (@olearycrew added this last week), then give everyone in that cohort the $1. That way you have complex code defining the cohort, but that can do real cheap sql writing.
And then the actualy iteration code is easier. And it would make it easier to also query them for mailing them or something
The idempotent credit grant (onConflictDoNothing) already handles re-runs safely, making the resume cursor unnecessary complexity. Also fix misleading dollar amount in final stats log.
Phase 1 (cohort script): Tags eligible inactive users into a 'cli-v1-rollout' cohort via cheap SQL updates. Separates targeting logic from credit granting. Phase 2 (grant script): Iterates cohort members and grants credits. Simple, naturally resumable (idempotent grants skip already-credited users), and the cohort can be reused for mailing etc.
| const BATCH_SIZE = 10; | ||
| const SLEEP_AFTER_BATCH_MS = 1000; |
There was a problem hiding this comment.
I'd try to have this much lower. I think we needed it this high because of Orb, but there's no Orb anymore. Lowering should make the script run much faster
RSO
left a comment
There was a problem hiding this comment.
Small comments that I hope will make this script run faster. Let's try a dry run?
- Fix SQL syntax in cohort UPDATE query (use plain column names in SET clause) - Add ::text cast for cohort name parameter to help PostgreSQL type inference - Fix typo: grantCreditsToCohor -> grantCreditsToCohort - Remove pLimit dependency and rate limiting (no longer needed without Orb) - Make scripts testable by guarding auto-execution - Add comprehensive integration tests for both phases and end-to-end flow
efd4d1b to
08482d4
Compare
|
|
||
| while (hasMore) { | ||
| const cohortFilter = sql`${kilocode_users.cohorts} ? ${options.cohortName}`; | ||
| const users: User[] = await db.query.kilocode_users.findMany({ |
There was a problem hiding this comment.
WARNING: Blocked users may still receive credits
grantCreditsToCohort() queries only by cohort membership. If a user is tagged and later becomes blocked, they’d still be processed here. Consider filtering blocked_reason IS NULL in the query (or skipping inside the loop) to avoid granting promos to blocked accounts.
|
|
||
| // In dry run, just count cohort members | ||
| const cohortFilter = sql`${kilocode_users.cohorts} ? ${COHORT_NAME}`; | ||
| const users = await db.query.kilocode_users.findMany({ where: cohortFilter }); |
There was a problem hiding this comment.
WARNING: Dry-run loads entire cohort into memory
In dry-run mode this does findMany() with no limit, which can materialize a very large array and OOM for big cohorts. Prefer a COUNT(*) query (or stream/paginate similarly to the apply mode) so dry runs are safe at scale.
| const cutoffIso = new Date(Date.now() - options.inactiveDays * 24 * 60 * 60 * 1000).toISOString(); | ||
| const now = Date.now(); | ||
|
|
||
| const everUsed = sql`(SELECT DISTINCT ${microdollar_usage.kilo_user_id} FROM ${microdollar_usage})`; |
There was a problem hiding this comment.
WARNING: Cohort selection counts any microdollar_usage rows (including $0 / failed requests)
The SQL treats any microdollar_usage row as "used Kilo". If $0-cost rows include auth failures (e.g. OpenRouter 401s), users who never successfully used Kilo could be tagged, and recent failed requests could exclude otherwise-eligible users. Consider filtering to meaningful usage (e.g. WHERE cost > 0, and possibly organization_id IS NULL depending on the intended cohort).
| } else { | ||
| granted++; | ||
| } | ||
| } catch { |
There was a problem hiding this comment.
SUGGESTION: Swallowed errors make grant failures hard to diagnose
catch {} drops the error object, so debugging is limited to a list of user IDs. Consider capturing/logging error (or at least String(error)) for faster diagnosis when a grant fails unexpectedly.
.