# Diagnostics

Every TT-SDK code the SDKs can emit, what each one means, and what to change when you see it.

Source: https://tokentriage.com/docs/sdks/diagnostics

---
import Callout from '../../../components/Callout.astro';
import NextSteps from '../../../components/NextSteps.astro';
import Details from '../../../components/Details.astro';

Every message either SDK emits carries a stable `TT-SDK-###` code. The codes are frozen and
identical across both languages, so a search for one finds the same answer whichever binding
produced it.

This page is the complete list. If you arrived holding a code, the tables below are ordered so
the common ones come first.

## How they behave

Diagnostics go to a named logger, one per code per process by default. That rate limit exists
because these fire on a request path: a dimension value that fails the charset guard fails it on
every request, and an unrated warning would be the loudest thing in your logs.

| Setting | Effect |
|---|---|
| `TOKENTRIAGE_LOG=warn` | the default; one message per code per process |
| `TOKENTRIAGE_LOG=debug` | lifts the rate limit, so every occurrence prints |
| `TOKENTRIAGE_LOG=off` | silences everything except the kill-switch line |

Turn `debug` on when you are diagnosing which requests are affected rather than whether any are.

Two properties are worth knowing. Messages are ASCII, so they survive any console and any CI log.
And `TT-SDK-030` prints even at `off`, because a kill switch that silences itself produces empty
dashboards nobody can explain.

## The ones you will actually see

| Code | Means | What to do |
|---|---|---|
| `TT-SDK-012` | the response had no `x-tt-request-id` | point `base_url` at the proxy, not the provider |
| `TT-SDK-013` | `last_receipt()` called outside any scope | read it inside the scope that made the call |
| `TT-SDK-011` | an adapter skipped a request off your `base_url` origin | nothing, usually; this is the leak guard working |
| `TT-SDK-001` | a value was truncated to its byte cap | shorten the value, or accept the truncation |
| `TT-SDK-008` | a value or tag key was dropped for its characters | see [the charset guard](/docs/sdks/emission#value-rules) |
| `TT-SDK-006` | `init()` was called after first use | configure in one place at startup |

`TT-SDK-012` is the one that means something is wrong most often. `TT-SDK-011` is the one most
often mistaken for a problem: it means an instrumented client made a request somewhere else and
your identity headers correctly stayed at home.

## Emission

| Code | Fires when |
|---|---|
| `TT-SDK-001` | a value exceeded its byte cap and was truncated |
| `TT-SDK-002` | `step_class` was not in `user_turn`, `tool_loop` or `provider_state` |
| `TT-SDK-003` | the per-call `cache` control was not in `off`, `refresh` or `no-store` |
| `TT-SDK-004` | a tag key exceeded 32 bytes and was truncated |
| `TT-SDK-005` | `traceparent` was skipped because the ids were not 32/16 lowercase hex |
| `TT-SDK-008` | a value carried a byte outside `0x20`–`0x7E`, or a tag key was outside `[a-z0-9_-]` |
| `TT-SDK-010` | a value was absent or could not be coerced |
| `TT-SDK-014` | a non-default `tier_header` is configured |

All eight drop or trim one header and leave the rest of the request alone. None of them raises.

`TT-SDK-014` is a reminder rather than a fault: a renamed tier header is stripped by the proxy
only if your server config names the same header, and if it does not, the header reaches your
provider.

## Scopes and context

| Code | Fires when |
|---|---|
| `TT-SDK-007` | a `trace` was opened inside another `trace` |
| `TT-SDK-013` | `last_receipt()` was called outside any scope |
| `TT-SDK-018` | **TypeScript only**: the runtime has no `AsyncLocalStorage` |

<Callout type="warning">
`TT-SDK-013` fires only outside **every** scope. Reading a receipt inside a trace but after the
step that made the call has closed returns nothing and emits nothing, because you are still
inside a scope. That silent case is the most common instrumentation bug in this SDK, and
[which scope holds it](/docs/sdks/receipts#which-scope-holds-it) is the rule.
</Callout>

`TT-SDK-018` means traces and steps run but do not propagate, so rows arrive standalone.
[Where propagation stops](/docs/sdks/context#where-propagation-stops) covers what still works.

## Adapters

| Code | Fires when |
|---|---|
| `TT-SDK-009` | **Python only**: a CrewAI agent could not carry headers |
| `TT-SDK-011` | an adapter declined to inject on a request off the configured origin |

`TT-SDK-009` names how many agents of how many were skipped, so a partly instrumented crew is
visible rather than silently partial.

## The doctor

| Code | Fires when |
|---|---|
| `TT-SDK-015` | `check()` reports the config-lint result |
| `TT-SDK-016` | `check()` reached the admin plane, and what it disclosed |
| `TT-SDK-017` | `check()` could not verify the admin plane, and why |

`TT-SDK-017` carries the reason: unreachable, requires auth, rejected token, an HTTP status, an
invalid URL, or skipped because of a plane collision. With `probe=false` it says the probe was
disabled rather than that anything failed.

## The two that mean something bigger

| Code | Means |
|---|---|
| `TT-SDK-000` | an internal fault was caught by the shield |
| `TT-SDK-030` | the SDK is turned off by `TOKENTRIAGE_DISABLED` |

`TT-SDK-000` names the entry point and the exception type. It means the SDK hit a bug and returned
its inert value — an empty header map, an all-absent receipt, a no-op scope — instead of raising
into your request path. Your request went out; it may have gone out unlabelled. Please report the
entry point and exception type, since a shielded fault is a defect that has been made survivable
rather than one that has been fixed.

`TT-SDK-030` prints once at init, even with logging off:

```text
TT-SDK-030: TokenTriage SDK DISABLED (TOKENTRIAGE_DISABLED) - headers() and adapters are no-ops
```

While disabled, `headers()` returns an empty map and adapters are inert. Receipt parsing keeps
working, so reading a response stays safe.

<Details summary="Why the message text is frozen, and the two places it is not">

Message text is frozen across languages so that a diagnostic can be searched, alerted on and
documented once. Codes never vary at all, which is why every cross-language assertion matches on
the code rather than the prose.

Two substitutions are allowed, and only two. A binding spells the API you actually called in its
own convention, so `last_receipt()` becomes `lastReceipt()`. And it uses its own literal for
absence, so `None` becomes `null`. `TT-SDK-010` and `TT-SDK-013` are the only affected codes.

The reason it is a carve-out rather than drift: a diagnostic exists to be acted on, and telling a
TypeScript developer that `last_receipt()` returned `None` names two things that do not exist in
their program.

Codes `019` through `029` are reserved and unused.

</Details>

<NextSteps items={[
  { title: 'From your call to the wire', href: '/docs/sdks/emission', description: 'The value rules behind the emission codes.' },
  { title: 'Reading the receipt', href: '/docs/sdks/receipts', description: 'TT-SDK-012 and TT-SDK-013 in context.' },
  { title: 'Install and configure', href: '/docs/sdks/install', description: 'Setting TOKENTRIAGE_LOG, and running the doctor.' },
  { title: 'The contract and how it evolves', href: '/docs/sdks/contract', description: 'What "frozen" means here, and what may change.' }
]} />
