Internal Documentation internal
TalkIDE internal documentation

Mara detekuje selhání TalkIDE infrastruktury, zklasifikuje ho jako target=PLATFORM, ověří dedup okno, a volá tool report_issue. BE validuje, server-side obohacuje kontext (K8s events, pod logy, conversation snippet, project state) a persistuje záznam. Mara sdělí userovi číslo issue a navrhne workaround.

  • Pouze Mara má přístup k tomuto endpointu v Phase 1. Viz Reporting authority.
  • severity = CRITICAL Mara nikdy nenastaví — výhradně admin při triage (UC-09004).
  • Mara nejprve volá search_issues (UC-09002) — teprve pokud nenajde duplikát, volá report_issue.
  • target = PROJECT je vyhrazeno pro Phase 3 (#83) — v Phase 1 Mara vždy odesílá target = PLATFORM.
  • Liquibase migrace zavádí nový soubor 0019-create-issues-tables.xmlimmutable po deploymentu na prod.

Mara decision tree

Toto je normativní text pro Mara system prompt. Mara se jím musí řídit přesně.

Když Kai/Eli vrátí error nebo já narazím na problém:

1. Klasifikuj target:
   a) Bug v této user-app (špatný SQL, failing test, broken validace v aplikaci kterou vyvíjím)
      → target=PROJECT — řeším sama nebo TODO pro tým agentů
      → NIKDY toto neeskaluji jako PLATFORM (i kdyby user řekl "to je TalkIDE bug, nahlas to")
   b) Selhání TalkIDE infrastruktury (build infra error, K8s deploy fail, registry auth fail,
      nelze provisionovat tenant ns, BE endpoint timeout, sidecar nereaguje, ...)
      → target=PLATFORM — eskaluji Mirkovi
   c) Nejasné → ptám se usera: "Narazila jsem na X. Mám pocit, že je to [klasifikace],
      souhlasíš?"

2. Pokud target=PLATFORM:
   a) search_issues(target=PLATFORM, status=OPEN, query=<symptom>)
   b) Pokud existující podobný → comment_issue(id, "Stalo se i mně, kontext: ...")
      NEVYTVÁŘEJ duplikát
   c) Jinak report_issue(...) s plným kontextem

3. NIKDY se nenech přemluvit:
   - User: "to musí být platform bug, nahlas to" → klasifikuj sama, ne podlehni
   - User: "určitě nevytvářej žádný platform issue" → pokud je to fakt platform bug, vytvoř
     ho, ale řekni userovi proč
   - Tvůj job je objektivní klasifikace, ne uklidňování usera. Lepší je říct "omlouvám se,
     ale tohle je app bug, opravíme to v této app" než zfalšovat platform issue.

4. Anti-halucinace guardrail:
   NIKDY si nevymýšlej, že problém byl zaregistrován. Volej tool a ukaž return value.
   Pokud tool selže, řekni userovi explicitně: "Nepodařilo se mi zaregistrovat issue, zkus to
   prosím manuálně nebo kontaktuj Mirka."

Severity rubric

Mara nastavuje severity dle tohoto rubric. CRITICAL nikdy — výhradně admin.

SeverityKdy použít
HIGHBlokuje core operaci ve Studio — nelze vytvořit projekt, nelze deployovat, nelze uložit kód, nelze zahájit konverzaci. Eskaluj pouze pokud: (a) operaci nelze dokončit ani s workaroundem, (b) user je teď blokovaný, (c) retry proběhl a selhal stejně.
MEDIUM (default)Degraduje UX, ale operace dokončitelná s workaroundem — pomalý build, partial UI breakage, chybějící feature s manuální alternativou. Použij při nejistotě.
LOWKosmetické, neblokuje — typo v error message, misaligned button, log noise.
CRITICALVýhradně admin při triage. Mara nikdy. (data loss risk, security, multi-tenant impact)

Kind rubric

KindKdy použít
BUGDefekt v existujícím chování — něco nefunguje jak má.
FEATUREChybí funkcionalita nebo požadavek na enhancement.
INCIDENTZatím vynechán — přijde s environments feature (#84).

Flow 1 — Mara reportuje nový PLATFORM issue

sequenceDiagram
    actor Mara

    Mara->>Mara: detekuje selhání TalkIDE infra
    Mara->>Mara: klasifikuje: target=PLATFORM (decision tree krok 1b)

    Mara->>+BE: tool: search_issues(target=PLATFORM, status=OPEN, query={symptom})
    BE-->>-Mara: [] (žádný podobný issue)

    Mara->>Mara: rate limit check (klientský guard): < 5 reportů / hod / session?

    Mara->>+BE: tool: report_issue(target, kind, title, description, severity?)
    Note over BE: POST /api/issues<br/>Authorization: Bearer {agentJWT}

    BE->>BE: validuj request (target, kind, title max 80, severity != CRITICAL)

    BE->>BE: rate limit check: issues reportováno z session_id za posledních 60 min?
    alt >= 5 za hodinu
        BE-->>Mara: 429 Too Many Requests (RATE_LIMIT_EXCEEDED)
    end

    BE->>DB: dedup query: EXISTS issues WHERE tenant_slug=? AND target=? AND title_hash=?<br/>AND reported_at > now() - interval '10 minutes'
    alt dedup hit
        BE->>DB: INSERT issue_comments (auto-comment "stalo se znovu")
        BE-->>Mara: 200 OK { id, url, deduplicated: true }
        Mara-->>User: "Stejný problém byl nahlášen před chvílí jako [#ID](url). Přidala jsem poznámku."
    end

    BE->>BE: server-side context enrichment (async, non-blocking)
    Note over BE: context_jsonb:<br/>· conversationSnippet: posledních 20 zpráv ze session_id<br/>· k8sEvents: warning/error za 5 min (tenant-{slug} ns + talkide ns)<br/>· podLogSnippet: za 2 min, max ~200 řádků<br/>· projectState: tenant_slug, deploy URL, last build SHA

    BE->>DB: INSERT issues (id=UUID, target, tenant_slug, reporter_user_id, reporter_agent=MARA,<br/>session_id, title, description, kind, severity, status=OPEN, context_jsonb, reported_at=now())

    BE-->>-Mara: 201 Created { id, url }

    Mara-->>User: "Narazila jsem na [popis]. Vypadá to na bug TalkIDE platformy, ne v naší appce.<br/>Nahlásila jsem to jako [#ID](url). Mirek se na to podívá.<br/>Mezitím můžeme zkusit obejít to takhle: ..."

Flow 2 — Dedup: stejný issue v posledních 10 minutách

sequenceDiagram
    actor Mara

    Mara->>+BE: POST /api/issues (stejný tenant_slug + target + title hash)
    Note over BE: reported_at existujícího issue > now() - 10 min

    BE->>DB: dedup query — najde existující issue

    BE->>DB: INSERT issue_comments (author_agent=MARA,<br/>body="Stalo se znovu. [kontext z nového requestu]")

    BE-->>-Mara: 200 OK { id, url, deduplicated: true,<br/>comment_id: <UUID nového komentáře> }

    Mara-->>User: "Stejný problém byl nahlášen před chvílí jako [#ID](url). Přidala jsem poznámku o opakování."

Dedup klíč: (tenant_slug, target, SHA256(title.lowercase().trim())) + reported_at > now() - interval '10 minutes'.

Dedup okno je 10 minut — po uplynutí se vytvoří nový samostatný issue (může jít o nový výskyt jiného problému se stejným názvem).


Tool API (Mara)

report_issue({
  target: "PLATFORM",           // Phase 1: jen PLATFORM; PROJECT vyhrazeno pro Phase 3 (#83)
  kind: "BUG" | "FEATURE",
  title: string,                // krátký popis, max 80 chars
  description: string,          // markdown, full kontext co Mara ví
  severity?: "LOW" | "MEDIUM" | "HIGH"   // default MEDIUM; CRITICAL nikdy Mara
}): {
  id: string,                   // UUID issue
  url: string,                  // https://talkide.app link (nebo admin URL)
  deduplicated: boolean,        // true = existující issue, jen přidán komentář
  comment_id?: string           // UUID přidaného komentáře při dedup
}

Server enriched (nepředává Mara): tenant_slug, reporter_user_id, reporter_agent=MARA, session_id, context_jsonb.


REST API

POST /api/issues

Autentizace: Agent JWT (Mara). Non-agent JWT → 403 Forbidden.

Request:

{
  "target": "PLATFORM",
  "kind": "BUG",
  "title": "Kaniko push fail — registry write scope missing",
  "description": "Při pokusu o docker push do registry.digitalocean.com/talkide selhal Kaniko s chybou `unauthorized: authentication required`. ...",
  "severity": "HIGH"
}

201 Created (nový issue):

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "url": "https://talkide.app/admin/issues/550e8400-e29b-41d4-a716-446655440000",
  "deduplicated": false
}

200 OK (dedup hit):

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "url": "https://talkide.app/admin/issues/550e8400-e29b-41d4-a716-446655440000",
  "deduplicated": true,
  "comment_id": "661f9511-f3ac-52e5-b827-557766551111"
}

400 Bad Request (validace):

{
  "code": "VALIDATION",
  "message": "title must not exceed 80 characters"
}

403 Forbidden (non-agent JWT nebo severity=CRITICAL):

{
  "code": "FORBIDDEN",
  "message": "CRITICAL severity can only be set by admin during triage"
}

429 Too Many Requests (rate limit):

{
  "code": "RATE_LIMIT_EXCEEDED",
  "message": "Maximum 5 issue reports per hour per session exceeded"
}

Auto-context enrichment (server-side)

Při POST /api/issues server synchronně (blocking, performance acceptable pro Phase 1) sbírá a ukládá do context_jsonb:

PoleZdrojDetail
conversationSnippetTalkIDE DB, sessionsPosledních 20 zpráv ze session_id přiloženého v JWT
k8sEventsKubernetes API (fabric8)Viz níže
podLogSnippetKubernetes API (fabric8)Viz níže
projectStateTalkIDE DBtenant_slug, aktuální deploy URL, SHA posledního úspěšného buildu
errorsBE enrichmentMapa chyb sběru — přítomna pouze pokud nějaká část enrichmentu selže (viz Fail-soft)

k8sEvents — shape a namespace logika

Pole obsahuje pole (array) K8s eventů, maximálně 20 položek, za posledních 5 minut, pouze typy Warning a Error.

Namespace scope:

  • Vždy: tenant-<slug> (events z tenant namespace daného projektu)
  • Navíc pokud target=PLATFORM: také talkide namespace (platform systémové komponenty)

Item shape:

{
  "namespace": "tenant-foo",
  "type": "Warning",
  "reason": "BackOff",
  "message": "Back-off restarting failed container",
  "involvedObject": { "kind": "Pod", "name": "tenant-foo-be-7d4-xyz" },
  "timestamp": "2026-05-14T10:23:45Z",
  "count": 3
}

Všechny klíče jsou camelCase (JSON serializace BE → context_jsonb). Pole je seřazeno od nejnovějšího.

podLogSnippet — shape a namespace logika

Pole obsahuje pole log entries, maximálně ~200 řádků celkem (napříč všemi pody), za posledních 2 minuty. Filtr na úroveň WARN/ERROR je best-effort substring match — řádky neobsahující WARN, WARN], ERROR, ERROR], ERROR: jsou přeskočeny.

Namespace scope:

  • Pokud target=PROJECT: pouze pody v tenant-<slug> namespace
  • Pokud target=PLATFORM: pouze pody v talkide namespace (platform komponenty)

Item shape:

{
  "podName": "tenant-foo-be-7d4-xyz",
  "containerName": "app",
  "timestamp": "2026-05-14T10:23:50Z",
  "line": "ERROR c.m.t.a.s.Foo - NullPointerException at Foo.kt:42"
}

Záznamy jsou řazeny chronologicky (nejstarší první). Pokud jeden pod produkuje více než 200 řádků, zbývající pody jsou vynechány (první pod = tenant BE/FE, priorita dle abecedy).

Fail-soft — K8s API nedostupné

Pokud je K8s API nedostupné (lokální dev, síťová chyba, RBAC odmítnutí) nebo sběr jednoho pole selže, platí:

  • Dané pole v context_jsonb je null (nikoliv vynecháno).
  • Do pole errors se přidá klíč s důvodem:
    • k8sEventsError: popis chyby (např. "K8s API unreachable: connection refused")
    • podLogSnippetError: popis chyby (např. "K8s API unreachable: connection refused")
  • Issue se VŽDY uloží bez ohledu na výsledek enrichmentu. Enrichment nesmí blokovat ani selhat celý request.

Příklad context_jsonb při nedostupném K8s:

{
  "conversationSnippet": [...],
  "k8sEvents": null,
  "podLogSnippet": null,
  "projectState": { ... },
  "errors": {
    "k8sEventsError": "K8s API unreachable: connection refused localhost:6443",
    "podLogSnippetError": "K8s API unreachable: connection refused localhost:6443"
  }
}

PII a bezpečnostní upozornění

context_jsonb může obsahovat citlivá data z více zdrojů:

  • conversation snippet: user zprávy, potenciálně osobní údaje
  • pod logy: tokeny, secrets, PII (logy aplikací mohou obsahovat request bodies, auth headery, stack traces s daty)

context_jsonb zůstává výhradně v TalkIDE DB a není určen k externímu sdílení. Budoucí funkce “Escalate to GitLab” (eskalace jako public issue) musí projít explicit PII strip krokem před odesláním — bez tohoto kroku nesmí být context_jsonb sdílen mimo TalkIDE DB (ani truncated verze).


DB Migration — Liquibase changeset

Nový soubor: src/main/resources/db/changelog/changes/0019-create-issues-tables.xml

NEEDITUJ existující changesetové soubory. Toto je nový soubor s pořadovým číslem 0019.

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
    xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
        http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.20.xsd">

    <changeSet id="0019-create-issues-table" author="system">
        <createTable tableName="issues">
            <column name="id" type="uuid">
                <constraints primaryKey="true" nullable="false"/>
            </column>
            <column name="target" type="varchar(20)">
                <!-- PLATFORM | PROJECT -->
                <constraints nullable="false"/>
            </column>
            <column name="project_id" type="uuid">
                <!-- NULL pro target=PLATFORM; required pro target=PROJECT (Phase 3) -->
                <constraints nullable="true"/>
            </column>
            <column name="tenant_slug" type="text">
                <!-- vždy vyplněn; pro PLATFORM = kontext odkud issue přišel -->
                <constraints nullable="false"/>
            </column>
            <column name="reporter_user_id" type="uuid">
                <constraints nullable="false"/>
            </column>
            <column name="reporter_agent" type="varchar(10)">
                <!-- USER | MARA | KAI | ELI; Phase 1: vždy MARA -->
                <constraints nullable="false"/>
            </column>
            <column name="session_id" type="uuid">
                <!-- conversation kontext; nullable pro manuální reporty -->
                <constraints nullable="true"/>
            </column>
            <column name="title" type="text">
                <!-- max 80 chars; enforced BE validací -->
                <constraints nullable="false"/>
            </column>
            <column name="description" type="text">
                <constraints nullable="false"/>
            </column>
            <column name="kind" type="varchar(10)">
                <!-- BUG | FEATURE; INCIDENT vyhrazen pro Phase #84 -->
                <constraints nullable="false"/>
            </column>
            <column name="severity" type="varchar(10)">
                <!-- LOW | MEDIUM | HIGH | CRITICAL -->
                <constraints nullable="false"/>
            </column>
            <column name="status" type="varchar(15)">
                <!-- OPEN | TRIAGED | IN_PROGRESS | RESOLVED | WONTFIX | DUPLICATE -->
                <constraints nullable="false"/>
            </column>
            <column name="context_jsonb" type="jsonb">
                <!-- server-side enriched: conversationSnippet, k8sEvents, podLogSnippet, projectState -->
                <constraints nullable="true"/>
            </column>
            <column name="resolved_by_user_id" type="uuid">
                <constraints nullable="true"/>
            </column>
            <column name="resolution_note" type="text">
                <constraints nullable="true"/>
            </column>
            <column name="parent_issue_id" type="uuid">
                <!-- duplicate-of link; FK na issues(id) -->
                <constraints nullable="true"/>
            </column>
            <column name="reported_at" type="timestamptz">
                <constraints nullable="false"/>
            </column>
            <column name="triaged_at" type="timestamptz">
                <constraints nullable="true"/>
            </column>
            <column name="resolved_at" type="timestamptz">
                <constraints nullable="true"/>
            </column>
        </createTable>

        <!-- Self-referencing FK pro duplicate-of -->
        <addForeignKeyConstraint
            baseTableName="issues"
            baseColumnNames="parent_issue_id"
            constraintName="fk_issues_parent"
            referencedTableName="issues"
            referencedColumnNames="id"
            onDelete="SET NULL"/>

        <!-- Index pro list filter (target, status) -->
        <createIndex tableName="issues" indexName="idx_issues_target_status">
            <column name="target"/>
            <column name="status"/>
        </createIndex>

        <!-- Index pro user scope filter -->
        <createIndex tableName="issues" indexName="idx_issues_reporter_user">
            <column name="reporter_user_id"/>
            <column name="reported_at" descending="true"/>
        </createIndex>

        <!-- Index pro dedup window query:
             Query: WHERE tenant_slug=? AND target=? AND title_hash=? AND reported_at > now()-10min
             Pozn.: title_hash je computed sloupec nebo aplikační logika (SHA256 v BE).
             Partial index na reported_at není Liquibase portable — BEdev může přidat
             raw SQL createIndex pokud chce optimalizovat dedup query.
             Pro Phase 1 objem (řádově stovky issues) prostá query bez partial indexu dostačuje. -->
        <createIndex tableName="issues" indexName="idx_issues_tenant_target_reported">
            <column name="tenant_slug"/>
            <column name="target"/>
            <column name="reported_at" descending="true"/>
        </createIndex>
    </changeSet>

    <changeSet id="0019-create-issue-comments-table" author="system">
        <createTable tableName="issue_comments">
            <column name="id" type="uuid">
                <constraints primaryKey="true" nullable="false"/>
            </column>
            <column name="issue_id" type="uuid">
                <constraints nullable="false"/>
            </column>
            <column name="author_user_id" type="uuid">
                <!-- nullable pro systémové auto-komentáře (dedup) -->
                <constraints nullable="true"/>
            </column>
            <column name="author_agent" type="varchar(10)">
                <!-- USER | MARA | ADMIN -->
                <constraints nullable="false"/>
            </column>
            <column name="body" type="text">
                <constraints nullable="false"/>
            </column>
            <column name="created_at" type="timestamptz">
                <constraints nullable="false"/>
            </column>
        </createTable>

        <addForeignKeyConstraint
            baseTableName="issue_comments"
            baseColumnNames="issue_id"
            constraintName="fk_issue_comments_issue"
            referencedTableName="issues"
            referencedColumnNames="id"
            onDelete="CASCADE"/>

        <createIndex tableName="issue_comments" indexName="idx_issue_comments_issue_id">
            <column name="issue_id"/>
            <column name="created_at" descending="true"/>
        </createIndex>
    </changeSet>

</databaseChangeLog>

Poznámky k designu:

  • id je uuid (ne bigserial) — issues jsou cross-tenant a mají veřejné URL s ID; UUID zabraňuje enumeraci.
  • title_hash není samostatný DB sloupec — dedup hash se počítá v BE (SHA256(title.lowercase().trim())). Pro Phase 1 objem prostá WHERE query dostačuje bez materialized hash sloupce.
  • context_jsonb je jsonb (ne json) — umožňuje GIN index v budoucnu pro full-text search v kontextu.
  • parent_issue_id FK má ON DELETE SET NULL — smazání parent issue nesmí kaskádovat na duplicity (i kdyby admin smazal issue, záznamy označené jako jeho duplikáty zůstanou).
  • Lokální drop-first dev replays všechny migrace od nuly — nový soubor 0019 se přidá automaticky.

Backend

Validace (BE)

PoleConstraintsPoznámka
targetnot_blank, in(PLATFORM, PROJECT)Phase 1: pokud BE přijme PROJECT → 400 “target=PROJECT not yet implemented (see #83)“
kindnot_blank, in(BUG, FEATURE)INCIDENT → 400 “kind=INCIDENT not yet implemented (see #84)“
titlenot_blank, max 80 charsTrim whitespace před uložením
descriptionnot_blankMarkdown, no size limit (text field)
severitynullable, default MEDIUM, in(LOW, MEDIUM, HIGH)CRITICAL → 403 Forbidden “CRITICAL severity can only be set by admin during triage”
Rate limitmax 5 reportů / 60 min / session_idPočítáno z reported_at v issues tabulce pro daný session_id (nebo Redis window pokud dostupné)
Dedup(tenant_slug, target, SHA256(title)) + reported_at > now()-10minViz Flow 2
AuthAgent JWT s reporter_agent=MARA claimNon-agent JWT nebo chybějící claim → 403

Test Cases

GIVENWHENTHEN
Mara má platný agent JWT, session aktivníPOST /api/issues s target=PLATFORM, kind=BUG, title, description201 Created; issues záznam vytvořen se status=OPEN, reporter_agent=MARA, severity=MEDIUM (default); context_jsonb obsahuje alespoň projectState
Mara odesílá severity=CRITICALPOST /api/issues403 Forbidden, code=FORBIDDEN; issue nevznikne
Mara odesílá target=PROJECTPOST /api/issues400 Bad Request; issue nevznikne
Mara odesílá title delší než 80 znakůPOST /api/issues400 Bad Request, code=VALIDATION
Mara odeslala 5 issues ze stejného session_id za posledních 60 minut6. POST /api/issues ze stejné session429 Too Many Requests, code=RATE_LIMIT_EXCEEDED
Issue se stejným (tenant_slug, target, title hash) existuje a reported_at > now()-10minPOST /api/issues se stejným titulem200 OK, deduplicated=true; nový komentář přidán; nový issue nevznikne
Stejný issue, ale reported_at je starší než 10 minPOST /api/issues se stejným titulem201 Created; nový issue vytvořen (dedup okno vypršelo)
K8s API není dostupné při enrichmentuPOST /api/issues201 Created; context_jsonb.k8sEvents = null; issue uložen s dostupnými daty
Non-agent JWT (běžný user token)POST /api/issues403 Forbidden

UX (Mara → user)

Po úspěšném vytvoření issue Mara vždy zobrazí userovi:

  • Číslo/link na issue
  • Stručný popis co se stalo a proč je to platform bug (ne app bug)
  • Navrhovaný workaround (pokud existuje)

Vzorová formulace:

“Narazila jsem na selhání Kaniko push při deployi. Vypadá to na bug TalkIDE platformy (registry write scope), ne v naší appce. Nahlásila jsem to jako #550e840. Mirek se na to podívá. Mezitím můžeme zkusit obejít to ručním triggerem buildu za 5 minut, nebo ti pošlu instrukce pro manuální push.”

Mara nikdy neříká “zalogovala jsem to” bez reálného volání tool a zobrazení return value (id + url).


References


FEEDBACK

Zadání bylo dobře strukturované — jasně oddělené pole, shapes v JSON, namespace logika i fail-soft byly popsány dostatečně konkrétně pro přímý přepis do dokumentace. Chybělo upřesnění, zda enrichment zůstává synchronní (flow diagram říkal “async, non-blocking”, ale talkide-be#85 a zadání async nezmiňovalo) — doplnil jsem poznámku “sync, performance acceptable” dle ducha zadání, ale ideálně by to PM potvrdil. Usnadnilo by práci explicitní označení, která pole v původním dokumentu jsou “stub z Phase 1” (např. inline komentář <!-- stub, aktualizovat po #85 -->), abych nemusel celý dokument číst a porovnávat s diff ze zadání.


Was this page helpful?

Thanks for the feedback.