Skip to content

URL: https://mkdocs.justinsforge.com/memory/general/reference_creatortrack_warm_slots/

CreatorTrack warm dev-slot pool

Three pre-booted CreatorTrack dev slots kept warm (worktree up + next dev bound + app shell compiled) so a claim hands a serving preview to an agent in seconds, not the minutes a cold forge_creatortrack_agent_bootstrap.sh costs. Ports 3067-3069, DBs ct_warm_slot_1..3, worktrees ~/forge-suite-wt/warm-slot-{1,2,3}. Sits just above the long-lived manual dev slot on :3066.

Registry is the single source of truth

forge/data/creatortrack_slots/slots.json (runtime state, gitignored; created by init). Canonical mutator: forge_creatortrack_slots_registry.py (flock-guarded, atomic temp-replace). Every pool tool reads/writes ONLY through it.

Per-slot: worktree, port, db, slot_branch (warm-slot-N), branch (current), state, claimed_at, warmed_at. States:

  • empty no worktree/server; the maintainer will (re)warm it (waits under pressure).
  • warm ready to claim in seconds.
  • claimed handed to an agent on a feat/ branch; counts toward the reaper cap.
  • parked a dirty slot whose server was stopped but worktree+DB preserved; never auto-re-warmed, needs release --force once the work is saved.

Slot identity is decoupled from the git branch on purpose. A claim runs git switch -c feat/x IN PLACE, so the branch changes but worktree/port/DB stay pinned (.env.local is read once at server start, so the switch never disturbs the live server). Deriving teardown paths from the branch (the legacy behavior) computed the wrong DB the instant a slot switched branches; the registry keeps the stable mapping.

Components

Piece File Role
Registry mutator scripts/forge_creatortrack_slots_registry.py init/show/rows/get/claim/set, flock-atomic
Bootstrap slot mode scripts/forge_creatortrack_agent_bootstrap.sh --slot <n> (re)warm slot n on warm-slot-n reset to main (-B), block-wait for bind, flip state=warm
Maintainer scripts/forge_creatortrack_warm_slots.sh + forge-ct-warm-slots user timer (15 min, enabled) ensure each slot claimed/parked/warm; (re)warm empty/dead; skips refills at >=85% mem used
Claim / release scripts/forge_creatortrack_slot_claim.sh claim <feat> atomic pick+switch+mark+URL; release (<n>\|<branch>) [--force] teardown + background re-warm; status
Teardown (registry-aware) scripts/forge_creatortrack_agent_teardown.sh --slot <n> (or branch autodetect) pinned worktree/port/DB, resets registry state (empty on full, parked on dirty preserve)
Reaper (memory-aware) scripts/forge_creatortrack_dev_slot_reaper.py see bands below

Memory-aware reaper bands (Justin amendment 2026-07-13)

Pressure = memory USED % from MemAvailable/MemTotal (not buff/cache-inflated "used").

Band Used % Idle reaping Warm spares Claimed cap
1 < 60 none exempt 5 (enforced)
2 60-85 idle timeout 45m -> 3h exempt 5 (enforced)
3 > 85 45m idle, incl. warm spares; maintainer skips refills reapable 5 (enforced)

Warm spares NEVER count toward the cap (cap is slot hygiene, not RAM). The reaper's recent-edit "active" signal is suppressed for warm spares (their git-checkout mtimes read as fresh edits, a false positive). Reaping a registry slot goes through teardown --slot <n>. Test bands without waiting/real pressure via FORGE_FAKE_MEM_PCT (or REAPER_FAKE_MEM_PCT) + --dry-run (+ seed idle ages in data/creatortrack/dev_slot_reaper_state.json).

Ops

  • Bring pool up / heal: forge_creatortrack_warm_slots.sh (idempotent; the timer does this).
  • Claim: forge_creatortrack_slot_claim.sh feat/my-thing -> prints preview URL.
  • Release: forge_creatortrack_slot_claim.sh release <slot|branch> (dirty -> preserved; add --force).
  • Status: forge_creatortrack_slot_claim.sh status.
  • Logs: logs/creatortrack_warm_slots.log, logs/creatortrack_dev_slot_reaper.log.
  • Units mirrored in infra/systemd/ (forge-ct-warm-slots.* user; reaper .service is a system unit at /etc, cap now 5).

Gotchas learned building this (2026-07-13)

  • Lock-fd inheritance: the maintainer's flock fd is inherited by the detached next dev it launches, which would hold the lock for the server's whole life and wedge every later tick. Fixed by closing the fd across the bootstrap call (9>&-). Any future lock-holder that spawns a detached server must do the same.
  • Warm-up compile: state flips to warm when the port BINDS; the background warm-up curl compiles the shell over the next up-to-180s, so by claim time it serves instantly. The maintainer block-waits for bind (off any interactive path).

Live-data dev base (2026-07-13)

creatortrack_dev_base is rebuilt DAILY from live prod (lifeos) by scripts/forge_creatortrack_dev_base_refresh.sh, called from the warm-slots maintainer tick with --no-lock --max-age-hours 24. Justin asked for dev clones to mirror what he actually has set up (2026-07-13).

  • Method: pg_dump lifeos | pg_restore into creatortrack_dev_base_next, table-count sanity check, atomic rename swap, drop old. Prod is read-only throughout.
  • MUST use the v17 client binaries (/usr/lib/postgresql/17/bin); PATH pg_dump is 16 and refuses the v17 server.
  • After the swap it re-clones UNCLAIMED warm slots' DBs so the pool is fresh at claim time. Claimed/parked slots are never touched (in-flight review state); they refresh on release. Slot dev servers survive the drop/recreate (same DB name, pool reconnects).
  • lifeos_admin was granted pg_signal_backend on CT109 (2026-07-13) so it can terminate slot-DB connections before drop; without it the drop fails loudly.
  • Branch migrations are NOT in the base (prod schema only); apply per slot: PGDATABASE=<slot-db> WORKSPACE_MIGRATIONS_DIR=db/migrations python3 ~/forge/scripts/forge_workspace_migrate.py.
  • Manual full refresh right now: forge_creatortrack_dev_base_refresh.sh (no flags).
  • Stamp: data/creatortrack_slots/dev_base_refreshed_at (UTC ISO).

Related: dev-slot reaper, teardown, parallel dev agents.

[Claude Code]