Skip to content

Add inactive user re-engagement rollout script#29

Open
marius-kilocode wants to merge 7 commits intomainfrom
cli-v1-rollout
Open

Add inactive user re-engagement rollout script#29
marius-kilocode wants to merge 7 commits intomainfrom
cli-v1-rollout

Conversation

@marius-kilocode
Copy link
Contributor

@marius-kilocode marius-kilocode commented Feb 5, 2026

.

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()));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)}`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
console.log(`\nTotal credits granted: $${stats.successful.toFixed(2)}`);
console.log(`\nTotal credits granted: $${(stats.successful * 1).toFixed(2)}`);

@kiloconnect
Copy link
Contributor

kiloconnect bot commented Feb 5, 2026

Code Review Summary

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 1

Fix these issues in Kilo Cloud

Issue Details (click to expand)

WARNING

File Line Issue
src/scripts/d2025-02-04_cli-v1-rollout-cohort.ts 32 Cohort selection counts any microdollar_usage rows (incl. $0/failed requests), which can tag the wrong users

SUGGESTION

File Line Issue
src/scripts/d2025-02-04_cli-v1-rollout-grant.ts 75 catch {} swallows errors, reducing diagnosability of unexpected grant failures
Files Reviewed (5 files)
  • src/lib/promoCreditCategories.ts - 0 issues
  • src/scripts/d2025-02-04_cli-v1-rollout-README.md - 0 issues
  • src/scripts/d2025-02-04_cli-v1-rollout-cohort.ts - 1 issue
  • src/scripts/d2025-02-04_cli-v1-rollout-grant.ts - 1 issue
  • src/tests/cli-v1-rollout.test.ts - 0 issues

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}`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would instead just let it only process users who dont have credits yet, this seems like nonsense

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was just copied from the previous rollout script

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not saying the previous script wasnt vibed :P

Copy link
Contributor

@markijbema markijbema left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Comment on lines 16 to 17
const BATCH_SIZE = 10;
const SLEEP_AFTER_BATCH_MS = 1000;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Contributor

@RSO RSO left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

while (hasMore) {
const cohortFilter = sql`${kilocode_users.cohorts} ? ${options.cohortName}`;
const users: User[] = await db.query.kilocode_users.findMany({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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})`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants