Tento dokument specifikuje mechanismus per-user spending quota pro Mara (PM agent) a Kai (devops agent) v TalkIDE platformě, UX vrstvu “Mara fatigue states” nad tímto mechanismem a globální emergency brake. Vychází z rozhodnutí zaznamenaných v ADR-020.
1. Quota Window Mechanics
TalkIDE měří spend každého uživatele ve dvou překrývajících se rolling windows. Obě musí
mít dostatek headroomu, aby turn proběhl — jinak vrátí ASLEEP.
1.1 5hodinová window
| Parametr | Hodnota |
|---|---|
| Trigger | první Mara zpráva po předchozím reset nebo po signupu |
| Trvání | 5 hodin od triggeru (pevně) |
| Po vypršení | window se resetuje; další zpráva startuje nové 5h okno |
| Idle user | nečerpá nic — window se nespustí, dokud nepošle zprávu |
| Default budget | $2.00 (≈ 1 heavy session ~30 promptů NEBO 2–3 light sessions) |
| Config klíč | talkide.fup.budget-5h-cents (int, v centech) |
Příklad: uživatel odešle první zprávu v 10:00 → window startuje na 10:00, expiruje 15:00. Pokud do 15:00 nevyčerpá budget, window expiruje a nová začne při příštím promptu.
1.2 Weekly window
| Parametr | Hodnota |
|---|---|
| Trigger | první zpráva v daném 7denním rolling intervalu |
| Trvání | 7 dní od triggeru |
| Po vypršení | nové 7denní okno při příští zprávě |
| Default budget | $10.00 (≈ 5–7 sessions/týden) |
| Config klíč | talkide.fup.budget-weekly-cents (int, v centech) |
Weekly window je tvrdší strop brání marathon abuse přes víc 5h sessions. Uživatel může
mít plný 5h budget, ale pokud weekly cap dosáhl 100 %, dostane ASLEEP.
1.3 Carry-over
Žádný carry-over. Nevyužitá kvóta z expirované window propadá. Use-it-or-lose-it. Cílem je férovost vůči ostatním v pool a prevence hromadění “power-day” burstu.
1.4 Grace finish
Kvóta se kontroluje před každým turnem, ne mid-turn:
- User pošle prompt → BE zkontroluje obě windows (5h + weekly)
- Pokud obě mají headroom → turn proběhne celý (i kdyby mezitím přesáhl limit)
- Pokud jedna nebo obě windows jsou na 100 % → okamžitý
ASLEEPresponse, turn se nespustí
Overhead “grace finish” je typicky 5–10 % nad kvótu. Akceptovatelné — alternativa (zastavit Maru uprostřed generování) je horší UX.
2. Mara Fatigue State Machine
Backend vypočte fatigue_level jako maximum z obou windows (použije přísnější z nich).
State se posílá v každé Mara odpovědi jako součást response envelopy.
2.1 State tabulka
| Window used | State | Avatar / styling | Reálný dopad na backend |
|---|---|---|---|
| 0–50 % | FRESH | Standardní, plná barva | Sonnet 4.5, full thinking, parallel tools |
| 50–70 % | WARMED_UP | Lehce přimhouřené oči | Sonnet 4.5, thinking cap 2k tokenů |
| 70–85 % | TIRED | Mírně bledší avatar | Haiku 4 pro simple tasky, Sonnet jen tool loops |
| 85–95 % | GROGGY | Zívající avatar, malátný styl | Haiku 4 všechno, no extended thinking, 500ms delay/req |
| 95–100 % | EXHAUSTED | Šedý avatar | Max 1 active conv, 5s delay/turn |
| 100 %+ | ASLEEP | Spící avatar, zamčený chat | Hard 429 + countdown |
2.2 API response envelope
Backend posílá strukturovaný state — žádný message pool v DB, žádný lokalizovaný text na BE straně:
{
"fatigue_level": "GROGGY",
"window_5h_pct": 88,
"window_weekly_pct": 71,
"sleep_until_ts": null
}
Pokud je state ASLEEP:
{
"fatigue_level": "ASLEEP",
"window_5h_pct": 100,
"window_weekly_pct": 95,
"sleep_until_ts": "2026-05-08T16:34:00Z"
}
sleep_until_ts = timestamp expiry bližší z vyčerpaných windows. FE z něj vypočítá
countdown (“Vrať se za 2h 34m”).
2.3 FE i18n a message pool
Frontend si z fatigue_level enumu vybere lokalizovanou message z vue-i18n souboru.
Každý state má pool 3–5 variant — FE pick random při render (ne při příchodu zprávy,
aby countdown neměnil text).
Příklady CS variant pro jednotlivé state (ilustrativní — finální copy je na FE):
| State | Příklady variant |
|---|---|
FRESH | ”Mara je v pohodě a připravená kódit” / “Mara je svěží, jdeme na to” / “Mara má plné baterky” |
WARMED_UP | ”Mara už chvíli pracuje, ale jede dál” / “Mara se rozjela” / “Mara je v tempu” |
TIRED | ”Mara je trochu unavená, šetří síly” / “Mara zpomaluje, ale táhne” / “Mara jede na druhý dech” |
GROGGY | ”Mara zívá. Možná jí dnes moc přemýšlení nejde.” / “Mara mhouří oči” / “Mara loví druhý dech” |
EXHAUSTED | ”Mara má motýlky před očima, drží se z posledních sil” / “Mara sotva stojí” / “Mara je na hraně” |
ASLEEP | ”Mara potřebuje vyspat. Vrať se za {countdown}.” / “Mara si dává pauzu, bude zpátky v {time}.” |
2.4 UX detail — Mara spí screen
Když uživatel otevře TalkIDE a Mara je ASLEEP, nezobrazí se prázdný chat s error hláškou.
Místo toho FE zobrazí fun full-screen (“Mara se vyspí v 14:23 — mezitím si můžeš prohlédnout
svoje projekty”). Chat input je disabled + locked. Countdown se updatuje real-time.
Po expiry sleep window (countdown = 0) FE automaticky unlockne chat — uživatel nemusí refreshovat.
3. Capacity Math per Anthropic Tier
3.1 Cena průměrného requestu (Sonnet 4.5 + prompt caching)
| Složka | Výpočet | Cena |
|---|---|---|
| Fresh input (5k tokenů) | 5k × $3/1M | $0.015 |
| Cached input (50k tokenů) | 50k × $0.30/1M | $0.015 |
| Output (1.5k tokenů) | 1.5k × $15/1M | $0.0225 |
| Průměr light request | ~$0.05 | |
| Průměr heavy request (extended thinking) | ~$0.10 |
3.2 Kapacitní tabulka
Předpoklady: weekly budget $10/user, WAU = 60 % registrovaných, safety cap 70 % monthly budget.
| Tier | Monthly cap | 70 % budget | Weekly budget | WAU | Registrovaní (WAU = 60 %) |
|---|---|---|---|---|---|
| Tier 2 | $500 | $350 | $81 | 8 | ~13 |
| Tier 3 | $1,000 | $700 | $163 | 16 | ~27 |
| Tier 4 | $5,000 | $3,500 | $814 | 81 | ~135 |
| Custom | — | — | — | neomezeno (smluvně) | — |
Klíčový insight: monthly $ cap udeří dřív než RPM/OTPM. Tier upgrade timeline (Tier 1 → Tier 4 = minimálně 21 dní) znamená, že invite cascade musí být striktně škálovaná podle aktuálního tieru — viz invite-system.md.
4. Gateway Architecture
Gateway NENÍ UseCase. Je to Spring
@Servicevolaný z UseCases. Toto rozdělení je důležité, protože Anthropic call je infrastructural concern (cross-cutting napříč více UC), ne business UC sám o sobě.
4.1 AnthropicGatewayService
Všechny Anthropic API cally musí jít přes jeden centrální entrypoint: AnthropicGatewayService
— Spring @Service bean. Žádný kód nesmí volat Anthropic API přímo mimo tento service.
Lokace: features/gateway/AnthropicGatewayService.kt
features/gateway/
├── AnthropicGatewayService.kt # centrální @Service entrypoint
├── FatiguePolicy.kt # model swap + delay logika
├── QuotaCheckService.kt # Redis R/W, window výpočty
├── ApiUsageLedger.kt # PG zápis po callu
└── PricingConfig.kt # pricing tabulka (verzovaná, update při price change)
4.2 Volající UseCases (concrete callers)
AnthropicGatewayService je v současnosti volán z těchto UseCases:
| UseCase | Lokace | Popis |
|---|---|---|
SendMessageUseCase | features/conversation/domain/SendMessageUseCase.kt | User pošle prompt do existující konverzace |
AgentStartConversationUseCase | features/conversation/domain/AgentStartConversationUseCase.kt | Explicitní start nové konverzace |
AutoStartConversationUseCase | features/conversation/domain/AutoStartConversationUseCase.kt | Automatický start konverzace |
Předpokládatelně přibudou další (project context summary, code search assistance apod.) —
všechny musí jít přes AnthropicGatewayService.
4.3 Diagram volání
┌─────────────────────────────────────────────────────────────┐
│ Controller layer │
│ (REST endpoint — /api/conversations/messages) │
└────────────────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ UseCase beans │
│ ┌──────────────────────┐ ┌──────────────────────────────┐ │
│ │ SendMessageUseCase │ │ AgentStartConversationUseCase│ │
│ └──────────┬───────────┘ └──────────────┬───────────────┘ │
│ │ │ │
│ ┌──────────▼──────────────────────────────▼──────────────┐ │
│ │ AutoStartConversationUseCase │ │
│ └────────────────────────┬───────────────────────────────┘ │
└───────────────────────────┼─────────────────────────────────┘
│ (všichni volají)
▼
┌─────────────────────────────────────────────────────────────┐
│ AnthropicGatewayService (@Service) │
│ features/gateway/AnthropicGatewayService.kt │
│ │
│ fun callAnthropic(userId, request): GatewayResponse { │
│ 1. quotaCheckService.check(userId) [Redis atomic] │
│ 2. fatigueResolver.resolve(userId) [policy] │
│ 3. anthropicClient.invoke(...) [SDK call] │
│ 4. usageLedger.persist(...) [PG] │
│ 5. return response + fatigue envelope │
│ } │
└─────────────────────────────────────────────────────────────┘
4.4 Kotlin sketch — Service + UseCase pattern
// features/gateway/AnthropicGatewayService.kt
@Service
class AnthropicGatewayService(
private val quotaCheckService: QuotaCheckService,
private val fatigueResolver: FatigueResolver,
private val anthropicClient: AnthropicClient,
private val usageLedger: ApiUsageLedger,
) {
fun callAnthropic(userId: Long, request: GatewayRequest): GatewayResponse {
// 1. Pre-check quota (Redis atomic)
// 2. Resolve fatigue state → apply slowdown policy
// 3. Anthropic API call (model swapped per fatigue level)
// 4. Compute cost from usage object
// 5. Atomic Redis decrement + PG ledger persist
// 6. Return GatewayResponse(content, fatigue_state, countdown_to_reset)
}
}
// features/conversation/domain/SendMessageUseCase.kt
@Component
class SendMessageUseCase(
private val gateway: AnthropicGatewayService,
private val conversationRepository: ConversationRepository,
// ...
) {
fun execute(input: SendMessageInput): SendMessageOutput {
// ... business logic (load conversation, build prompt context)
val response = gateway.callAnthropic(input.userId, gatewayRequest)
// ... persist message, return output with fatigue envelope for FE
}
}
4.5 Request flow
User prompt
│
▼
[1] Pre-check: QuotaCheckService.check(userId)
├── Redis WATCH/MULTI/EXEC (atomické)
├── Pokud ASLEEP → return 429 + sleep_until_ts (žádný Anthropic call)
└── Pokud OK → pokračuj
│
▼
[2] FatigueResolver.resolve(userId) → FatiguePolicy.applyPolicy(fatigue_level)
├── Model swap: Sonnet 4.5 → Haiku 4 (při TIRED/GROGGY/EXHAUSTED)
├── Thinking cap: limitovat extended thinking tokens
└── Delay injection (500ms u GROGGY, 5s u EXHAUSTED)
│
▼
[3] Anthropic API call (přes NetworkWorkerExecutor / Agent SDK worker)
│
▼
[4] Post-charge: PricingConfig.computeCost(usage)
├── usage.input_tokens × input_price
├── usage.cache_read_input_tokens × cache_read_price
├── usage.cache_creation_input_tokens × cache_write_price
└── usage.output_tokens × output_price
│
▼
[5] Atomic decrement: QuotaCheckService.recordSpend(userId, costCents)
└── Redis Lua script: decrement 5h bucket + weekly bucket atomicky
│
▼
[6] ApiUsageLedger.persist(ApiUsageRecord)
└── PostgreSQL INSERT do api_usage tabulky
4.3 Atomicita Redis operací
Bez atomicity proběhnou dva paralelní requesty stejného uživatele oba kontrolou a překročí kvótu. Řešení: Redis Lua script pro check-and-decrement v jedné atomické operaci.
-- Pseudokód Lua scriptu
local key5h = KEYS[1] -- user:{id}:5h_used_cents
local keyWk = KEYS[2] -- user:{id}:weekly_used_cents
local budget5h = ARGV[1]
local budgetWk = ARGV[2]
local used5h = tonumber(redis.call('GET', key5h) or 0)
local usedWk = tonumber(redis.call('GET', keyWk) or 0)
if used5h >= tonumber(budget5h) or usedWk >= tonumber(budgetWk) then
return {-1, used5h, usedWk} -- ASLEEP
end
return {1, used5h, usedWk} -- OK
4.4 Redis keys
| Key | Typ | Popis |
|---|---|---|
user:{id}:5h_window_start_ts | string (epoch ms) | Začátek aktuálního 5h okna |
user:{id}:5h_used_cents | int | Spotřeba v aktuálním 5h okně (centy) |
user:{id}:weekly_window_start_ts | string (epoch ms) | Začátek aktuálního weekly okna |
user:{id}:weekly_used_cents | int | Spotřeba v aktuálním weekly okně (centy) |
global:monthly_used_cents | int | Celková globální spotřeba v billing měsíci |
TTL na window keys: 5h key expiruje za 5 hodin od window_start_ts, weekly za 7 dní.
Redis expiry zajistí, že stale data neblokují nové okno.
4.5 PostgreSQL ledger
Tabulka api_usage — persistent audit trail per request:
CREATE TABLE api_usage (
id BIGSERIAL PRIMARY KEY,
req_id VARCHAR(64) NOT NULL UNIQUE, -- UUID v4 per request
user_id BIGINT NOT NULL REFERENCES users(id),
conversation_id BIGINT REFERENCES conversations(id),
requested_at TIMESTAMPTZ NOT NULL,
completed_at TIMESTAMPTZ,
model_used VARCHAR(64) NOT NULL, -- claude-sonnet-4-5, claude-haiku-4, ...
input_tokens INT NOT NULL DEFAULT 0,
output_tokens INT NOT NULL DEFAULT 0,
cache_read_tokens INT NOT NULL DEFAULT 0,
cache_write_tokens INT NOT NULL DEFAULT 0,
cost_cents INT NOT NULL DEFAULT 0, -- vypočteno z pricing tabulky
fatigue_state_at_request VARCHAR(20) NOT NULL, -- enum FRESH..ASLEEP
global_monthly_cents_at_request INT -- snapshot global counter
);
CREATE INDEX idx_api_usage_user_time ON api_usage (user_id, requested_at DESC);
CREATE INDEX idx_api_usage_conversation ON api_usage (conversation_id);
Slouží pro: analytics, support (co uživatel spotřeboval), weekly reconciliation job.
5. Global Emergency Brake
Per-user FUP nestačí — suma per-user budgetů je vždy větší než global budget (platforma
“counts on idle users”). Druhý layer ochrany pracuje s global:monthly_used_cents.
if global_used > 0.85 × monthly_cap:
→ "global slowdown" — všichni uživatelé posunutí o 1 stupeň fatigue
(FRESH → WARMED_UP, WARMED_UP → TIRED, atd.)
if global_used > 0.95 × monthly_cap:
→ freeze new conversations; jen pokračování existujících (FE zobrazí banner)
if global_used >= monthly_cap:
→ všechny Anthropic cally vrátí HTTP 503 "platform under maintenance"
(BE neposílá žádné cally na Anthropic)
monthly_cap je konfigurovatelný (talkide.fup.monthly-cap-cents). Při tier upgradu
stačí změnit config — žádná kódová změna.
Admin dashboard zobrazuje global progress bar v reálném čase (live poll z Redis).
6. Self-Tracked Spend
Platforma neověřuje spend jen z Anthropic console — ta má zpoždění a nepodporuje per-user granularitu. Místo toho:
- Každý Anthropic API response obsahuje
usageobjekt s token counts PricingConfigbean drží pricing tabulku (input, output, cache_read, cache_write) per model- BE počítá cost per request real-time při každém callu
- Pricing tabulka je verzovaná v configu — při Anthropic price change = config update + deploy
6.1 Pricing tabulka (k datu spuštění alpha)
Ceny v USD per milion tokenů:
| Model | Input | Cache read | Cache write | Output |
|---|---|---|---|---|
| claude-sonnet-4-5 | $3.00 | $0.30 | $3.75 | $15.00 |
| claude-haiku-4 | $0.80 | $0.08 | $1.00 | $4.00 |
6.2 Weekly reconciliation job
Každé pondělí ráno (scheduled, ShedLock protected) spustí reconciliation:
- Stáhne Anthropic Usage API za minulý týden (per-organization aggregate)
- Porovná se sumou
api_usage.cost_centsza stejné období - Pokud drift > 5 % → admin notification (email + dashboard alert)
- Výsledek uloží do
reconciliation_logtabulky
Cílem je detekovat pricing bug (špatná kalkulace) nebo neočekávané cally mimo gateway.
7. Mid-Conversation Budget Exhaustion
Kvóta může dojít mezi dvěma turny stejné konverzace. Chování:
| Situace | Chování |
|---|---|
| Quota OK před turnem | Turn proběhne celý, response se odešle |
| Quota dojde MID-TURN | Aktuální turn doběhne (grace finish), next turn vrátí ASLEEP |
| Quota vyčerpána před turnem | Okamžitý ASLEEP response, Mara nepíše nic |
FE po obdržení ASLEEP response:
- Zamkne chat input
- Zobrazí sleep screen s countdown
- Změní Mara avatar na spící
- Po expiry countdown automaticky odemkne (no refresh needed)
Konverzace zůstane v DB nedotčená — uživatel se může vrátit a pokračovat od místa, kde skončil.
References
- ADR-020 — architektonická rozhodnutí pro tento systém
- invite-system.md — founder-gated invite cascade a capacity plánování
- worker-runtime.md — MaraExecutor / NetworkWorkerExecutor, gateway-proxy a thin-seam kontrakt
- mara-context.md — Mara persona, CLAUDE.md rendering
Thanks for the feedback.