Skip to content

Playtime branch investigation and recovery

The reported 1,354-message branch had six recorded seconds, no new messages, and no original generation requests. Its messages were copied from its parent. The parent's time and the account's lifetime time were retained. The UI rounds six seconds to 0.0 h; multiplying cloned messages into additional hours would duplicate the parent's activity.

Tracking fixes

  • A failed tick followed by resume for the same lease previously reset the heartbeat without awarding the intervening time. Resume now awards elapsed time within the existing 90-second continuity limit. The client explicitly requests recovery only after an acknowledged resume in the current foreground window. Visibility/page restores reset continuity, including retries when a pause or restoration request was lost. Older clients keep the previous no-recovery behavior during rollout.
  • Whole-second rounding previously discarded the fractional remainder every tick. The next heartbeat now starts at the last awarded second, and captured analytics intervals use that same boundary.
  • A tick response from an old session could change the current branch's tracker state. Control sequences now reject stale tick responses, including responses arriving after the page becomes hidden. Hidden/disabled views cannot tick.
  • Restoring a page from the browser's back/forward cache now reacquires its lease.
  • Lifetime increments now enter a PostgreSQL pending table in the same transaction as the session tick. The batched flusher credits the account and removes the pending increment atomically; restart/retry does not lose or double-credit it. Old Redis pending values are drained for rolling deployment compatibility.

Historical estimation policy

packages/server/scripts/playtime-recovery-estimates.sql reads deduplicated original generation usage from ClickHouse. These rows are not copied when a story branches. User-wide ordering prevents overlapping estimated windows:

  1. Between consecutive requests in the same session, count the interval when it is no longer than ten minutes.
  2. For a first request, a session switch, or a longer break, estimate at most one minute immediately before the request, bounded by the previous request.
  3. Clip the first estimated minute to session creation and exclude ephemeral Studio sessions when checking the source database.
  4. Raise a session to the larger of its recorded time and its estimated total. Add only the difference to its account's lifetime total in the same transaction.

This is an activity-based estimate, not proof that a tab stayed visible between requests. Reading time without requests cannot be reconstructed. Existing recorded time is never reduced. Deleted sessions cannot be reconstructed from retained session counters. The source capture began in September, so historical date-range foreground intervals remain incomplete.

The recovery does not insert estimated foreground intervals into analytics. All-time counters include the recovered estimates through existing CDC views; date-range analytics retain observed intervals only. playtime_recovery_audit records each before-value, increase, policy, and evidence kind (estimate or recorded-session-floor). It is an operational audit table, not a runtime dependency.

Running the repair

Export the SQL result as { "policy": "usage-cadence-v1", "rows": [...] }. Supply the database URL through PLAYTIME_DATABASE_URL; no credentials belong in source files. The script uses the matching direct Neon endpoint because temporary staging tables cannot survive transaction-pool connection changes.

sh
node packages/server/scripts/recover-playtime.mjs input.json report.json --dry-run
# At least 60 seconds later, after reviewing the proposed increases:
node packages/server/scripts/recover-playtime.mjs input.json report.json --apply

The delay lets normal Redis lifetime batching catch up before reconciling the dry run's recorded-session floors. Application happens in 500-session transactions with lock and statement deadlines. Session changes, lifetime changes, and audit entries commit together. The policy/session key prevents reapplying a correction. Private evidence and reports stay in the ignored .codex/playtime-audit/ directory.

Verification

Regression tests first reproduced the reconnect loss, fractional rounding loss, stale parent-response race, and back/forward restoration failure. The local PGlite recovery test covers additive account updates, creation-time clipping, ephemeral exclusions, preserved parent/branch counters, account-floor repair, and a zero-change second run. A read-only ClickHouse fixture verified session switches, simultaneous events, and long-gap caps (240 and 30 seconds across two sessions, with no overlapping estimated time).

sh
pnpm --filter @yumina/app exec tsx --test src/features/chat/use-playtime-tracker.test.ts
node packages/server/scripts/recover-playtime.test.mjs
node packages/server/scripts/test-local.mjs src/routes/sessions-branch.test.ts src/lib/playtime-policy.test.ts src/lib/review-playtime.test.ts src/lib/lifetime-playtime-sql.test.ts
pnpm typecheck
pnpm build

The durable accumulator is additive migration 0057_playtime_lifetime_pending.sql; apply it before deploying the code. The delayed production schema self-heal also ensures it, while PGlite creates it before serving. Its isolated SQL test covers transaction rollback, failed flush/retry, repeated flush, session deletion, and account deletion. It works without Redis. The recovery repairs evidenced historical shortfalls but does not claim exact reconstruction of all previously unobserved time.

Production recovery result

Completed September 16, 2026 at 07:44:13 UTC. The repair raised 31,449 session counters by 26,239,888 estimated seconds (7,288.86 hours), plus 3,318 seconds of previously recorded session time missing from 97 lifetime counters. No totals were decreased. All eligible deficits in the fixed recovery snapshot were zero after application; newly arriving activity was allowed to continue throughout. The 31,449 sessions belonged to 9,282 accounts. PostgreSQL verification found zero corrected sessions below their audited target. ClickHouse's canonical session-time view contained the five largest corrections and retained the reported branch's six seconds. The additive pending table was installed empty in production during the initial repair, which did not deploy the runtime fixes. The subsequent release rebases those fixes onto the current main branch and renumbers the migration to 0057 to preserve Studio's 0056 migration.