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:
emptyno worktree/server; the maintainer will (re)warm it (waits under pressure).warmready to claim in seconds.claimedhanded to an agent on afeat/branch; counts toward the reaper cap.parkeda dirty slot whose server was stopped but worktree+DB preserved; never auto-re-warmed, needsrelease --forceonce 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.serviceis a system unit at /etc, cap now 5).
Gotchas learned building this (2026-07-13)¶
- Lock-fd inheritance: the maintainer's
flockfd is inherited by the detachednext devit 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
warmwhen 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_restoreintocreatortrack_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_adminwas grantedpg_signal_backendon 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]