Internal Documentation internal
TalkIDE internal documentation

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

ParametrHodnota
Triggerprvní 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 userneč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

ParametrHodnota
Triggerprvní 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:

  1. User pošle prompt → BE zkontroluje obě windows (5h + weekly)
  2. Pokud obě mají headroom → turn proběhne celý (i kdyby mezitím přesáhl limit)
  3. Pokud jedna nebo obě windows jsou na 100 % → okamžitý ASLEEP response, 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 usedStateAvatar / stylingReálný dopad na backend
0–50 %FRESHStandardní, plná barvaSonnet 4.5, full thinking, parallel tools
50–70 %WARMED_UPLehce přimhouřené očiSonnet 4.5, thinking cap 2k tokenů
70–85 %TIREDMírně bledší avatarHaiku 4 pro simple tasky, Sonnet jen tool loops
85–95 %GROGGYZívající avatar, malátný stylHaiku 4 všechno, no extended thinking, 500ms delay/req
95–100 %EXHAUSTEDŠedý avatarMax 1 active conv, 5s delay/turn
100 %+ASLEEPSpící avatar, zamčený chatHard 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):

StatePří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žkaVýpočetCena
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.

TierMonthly cap70 % budgetWeekly budgetWAURegistrovaní (WAU = 60 %)
Tier 2$500$350$818~13
Tier 3$1,000$700$16316~27
Tier 4$5,000$3,500$81481~135
Customneomezeno (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 @Service volaný 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:

UseCaseLokacePopis
SendMessageUseCasefeatures/conversation/domain/SendMessageUseCase.ktUser pošle prompt do existující konverzace
AgentStartConversationUseCasefeatures/conversation/domain/AgentStartConversationUseCase.ktExplicitní start nové konverzace
AutoStartConversationUseCasefeatures/conversation/domain/AutoStartConversationUseCase.ktAutomatický 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

KeyTypPopis
user:{id}:5h_window_start_tsstring (epoch ms)Začátek aktuálního 5h okna
user:{id}:5h_used_centsintSpotřeba v aktuálním 5h okně (centy)
user:{id}:weekly_window_start_tsstring (epoch ms)Začátek aktuálního weekly okna
user:{id}:weekly_used_centsintSpotřeba v aktuálním weekly okně (centy)
global:monthly_used_centsintCelková 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 usage objekt s token counts
  • PricingConfig bean 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ů:

ModelInputCache readCache writeOutput
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:

  1. Stáhne Anthropic Usage API za minulý týden (per-organization aggregate)
  2. Porovná se sumou api_usage.cost_cents za stejné období
  3. Pokud drift > 5 % → admin notification (email + dashboard alert)
  4. Výsledek uloží do reconciliation_log tabulky

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í:

SituaceChování
Quota OK před turnemTurn proběhne celý, response se odešle
Quota dojde MID-TURNAktuální turn doběhne (grace finish), next turn vrátí ASLEEP
Quota vyčerpána před turnemOkamžitý ASLEEP response, Mara nepíše nic

FE po obdržení ASLEEP response:

  1. Zamkne chat input
  2. Zobrazí sleep screen s countdown
  3. Změní Mara avatar na spící
  4. 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
Was this page helpful?

Thanks for the feedback.