# Gossipper Analyze

> Interpret Gossipper output artifacts (summary JSON, trace CSVs, call records JSONL, OTLP events) and produce a structured findings report. Use whenever the user wants to classify failures from a Gossipper run, evaluate latency or media quality verdicts, decide whether a run passed health thresholds, propose threshold values for the next run, convert RTCP jitter from RTP timestamp units to milliseconds, surface Call-IDs and time windows for HOMER/qryn correlation, interpret exit code 2, or read a "Run report" handed over from gossipper-run.

- Skill: `voicenterteam/gossipper-analyze` (Agent Skill, multi-file: 7 files)
- Install (CLI): `npx skillmds@latest add voicenterteam/gossipper-analyze`
- Raw SKILL.md: https://api.skillmd.com/api/skills/voicenterteam/gossipper-analyze/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- Author: VoicenterTeam (https://skillmd.com/u/voicenterteam)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/voicenterteam/gossipper-analyze

---


# gossipper-analyze — Output Interpretation

Interpret Gossipper outputs and produce structured findings. Pinned to **Gossipper 0.1.60-rtcpmuxpatch2** (sipcapture/gossipper on `main`, audited 2026-05-18).

## When to use this skill

- Read a "Run report" from gossipper-run and start an analysis.
- Classify failures from per-call records (`success: false` + `error` string) and summary counters; surface dominant failure cause.
- Compute latency verdicts (`invite_rtt`, `call_length`, custom `rtd` timers).
- Compute media-quality verdicts (RTCP loss, jitter, per-call RTP floor).
- Convert RTCP jitter fields (`media.RTCPMaxJitter` / `RTCPMinJitter` / etc., in RTP timestamp units) to ms.
- Decide whether a run passed health thresholds (or apply them post-hoc if `-health_*` wasn't set).
- Propose threshold values for the next run, by intent.
- Surface Call-IDs, time windows, `hep_capture_id`, `self_tag` for downstream HOMER/qryn correlation.
- Interpret exit code 2 and what to do about it.

## When NOT to use this skill

| Task | Owner |
|---|---|
| Invoke gossipper, pick flags, deploy | gossipper-run |
| Write or modify scenario XML | gossipper-build |
| Query HOMER/qryn ClickHouse directly | separate query layer (out of scope here) |
| Decide whether to escalate findings to humans / autonomous controllers | caller of this skill |

This skill reads files and produces verdicts. It does not run, write, or query.

## Inputs from gossipper-run

After every run, gossipper-run surfaces a structured "Run report" block in chat. Parse it — the file paths and correlation IDs come from there.

```
Run report
----------
Invocation:           <command line or profile path + alias>
Started / ended:      <ISO 8601> / <ISO 8601>
Exit code:            <0 | 1 | 2>

Outputs produced:
  summary_json:       <path>
  trace_stat:         <path or omitted>
  call_records_jsonl: <path or omitted>
  message_file:       <path or omitted>
  error_file:         <path or omitted>
  wav_dir:            <path or omitted>

Correlation identifiers:
  hep_capture_id:     <int or omitted>
  self_tag:           <id or omitted>
  hep_addr:           <host:port or omitted>
  otel_endpoint:      <host:port or omitted>

Notes: <operational context>
```

**Two things to check before opening any file:**

1. **Exit code** sets expectations.
   - `0` → summary JSON exists, clean run.
   - `1` → summary JSON may be missing. Read `*_err.log` first.
   - `2` → summary JSON exists with `health` + `findings` populated.

2. **Required channels.** If anything you need is missing, do not fudge — state what's needed and ask the user to request those flags on the next gossipper-run invocation:
   - `-summary_json <path>` — always required.
   - `-trace_stat -fd <n>` — required for time-series drill-down.
   - `-call_records_jsonl <path>` — required for per-call diagnostics.
   - `-trace_err -error_file <path>` — required when `transport_error` / `parse_error` shows up.

This skill does not control run's invocation. It tells the user what to ask for; run wires it.

## Analysis workflow

1. **Exit code first.** 0 / 1 / 2 sets expectations.
2. **Summary JSON next.** Read in order (field paths verified against `gossipper_summary_v1`, Gossipper 0.1.60-rtcpmuxpatch2):
   - `total_calls` / `success_ratio` → basic verdict.
   - `timeouts` + `retransmits` (top-level aggregate counters) → high-level signal. The summary does NOT carry a per-class `failures.{timeout|unexpected_sip|...}` breakdown — that comes from parsing per-call records.
   - `invite_rtt` + `call_length` (each is an object with `average`/`min`/`max`/`stddev`/`buckets`) + any custom `rtd` timer keys → signaling timing. **Latency values are in nanoseconds.** Divide by 1_000_000 for ms.
   - `media.*` → only if scenario uses RTP. Field names are **snake_case** in the summary (`rtp_packets_sent`, `rtcp_max_fraction_lost`, `rtcp_max_jitter_ts` if present). Convert any RTP-TS jitter to ms via codec clock. (See "Codec → ms" section.)
   - `findings[]` (always present as array of strings if summary is) and `health.passed` / `health.findings[]` → final verdict if `-health_*` thresholds were set at runtime.
3. **Drill into trace CSVs** if summary numbers raise questions:
   - `*_rtt.log` for per-call latency distribution / percentiles.
   - `*_stats.log` for time-series — when did failures spike?
   - `*_error_codes.log` for SIP-status breakdown.
4. **Per-call records JSONL** for individual failed-call diagnostics — find failed calls by `success == false` and pull their `call_id`. Each failed call has an `error` free-text string (e.g. `"sip recv: timed out after 27.997s ..."`); classify by regex if needed. **Field list is enumerated** in `references/output-schema-reference.md`; if `schema_version` differs from `gossipper_call_record_v1`, re-inventory via the drift-detection recipe in that doc.
5. **Surface correlation pointers** for downstream HOMER/qryn lookup. Do not attempt the query.
6. **Read `*_msg.log` / `*_err.log`** only when steps 1–5 don't explain it — last-resort context.

Full output-channel matrix and field references → `references/output-schema-reference.md`.

## Failure classification

The 0.1.60-rtcpmuxpatch2 binary does NOT emit a structured failure-class enum. Per-call records carry `success: bool` plus a free-text `error` string on failures. To classify, parse the `error` field with the regex table below — these classes are derived, not binary-native.

| Class | Match on `error` (case-insensitive substring or regex) | Means |
|---|---|---|
| `timeout` | `sip recv: timed out`, `recv timeout`, `timed out after` | Expected recv didn't arrive within `-recv_timeout_ms` (or `-recv_bye_timeout_ms` for BYE) |
| `unexpected_sip` | `unexpected`, `did not match`, `no matching recv` | SIP message didn't match current recv AND `_unexp.main` fallback didn't recover |
| `transport_error` | `connection refused`, `connection reset`, `i/o timeout` (transport context), `tls handshake` | Socket-level failure |
| `parse_error` | `parse error`, `malformed`, `invalid sip` | SIP message malformed beyond parser tolerance |
| `scenario_error` | `scenario`, `divide by zero`, `missing variable`, `bad xml` | Action failed at runtime |
| `cancelled` | `cancelled`, `canceled`, `context deadline exceeded` (run-level) | Run terminated externally while call was active |

First analysis question: count `success == false` records per derived class. Which class dominates?

Aggregate signal at summary level: only `timeouts` is broken out as a top-level counter; everything else (`failed_calls`, `success_ratio`) is undifferentiated. Lean on per-call parsing for breakdown.

Triage workflow per class → `references/patterns.md`.

## Codec → ms conversion (the jitter gotcha)

Per-call record fields (PascalCase): `media.RTCPMaxJitter`, `RTCPMinJitter`, `RTCPJitterSum` (with `RTCPJitterSamples` for averaging), and `RTPRecvInterarrivalJitterPeak`.

Summary JSON fields (snake_case, when present): `media.rtcp_max_jitter_ts`, `rtcp_avg_jitter_ts`, `rtcp_min_jitter_ts`, `rtp_recv_interarrival_jitter_peak_ts`.

Both surfaces report jitter in **RTP timestamp units, not milliseconds**. Conversion depends on the negotiated codec's sample rate.

| Codec(s) | Clock | Divide by |
|---|---|---|
| PCMU / PCMA / G722 / iLBC | 8 kHz | 8 |
| OPUS | 48 kHz | 48 |
| H264 | 90 kHz | 90 |

Always report jitter in ms. Always state the codec assumption.

**Codec inference rules:**

1. **Built-in scenarios (`-sn uac`, `-sn uas`):** assume **PCMU at 8000 Hz, 20 ms ptime**. Record `codec_assumed` in the findings report's `concerns[]` (see "Concerns schema" section below). Do **not** prompt the user.
2. **Custom scenarios (`-sf <file>`):** if the SDP / scenario doesn't declare a codec, ask the user. Built-in defaults do NOT apply.
3. **Override:** user may supply `codec=<g711a|g722|opus|...>` as an analyze-time hint, or annotate the run-profile JSON with a `codec_assumption` field. Override always wins; record the user's value in findings `inputs.codec_assumption`.

## Health threshold authoring

When the user asks for threshold values for `-health_*` on the next run, propose by intent. The user (or caller) confirms; gossipper-run wires them. **This skill never sets flags itself.**

Sample threshold sets per run intent → `references/patterns.md` (SOC monitor / soak / regression / smoke).

`-health_*` flag list:

| Flag | Compared against | Pass condition |
|---|---|---|
| `-health_min_success_ratio <0..1>` | summary `success_ratio` | `>= threshold` |
| `-health_max_failed_calls <n>` | summary `failed_calls` | `< threshold` (0 = any failure fails) |
| `-health_max_timeouts <n>` | summary `timeouts` (top-level counter) | `<= threshold` |
| `-health_max_rtcp_fraction_lost <0..1>` | summary `media.rtcp_max_fraction_lost` (when present) | `<= threshold` |
| `-health_max_rtcp_jitter_ts <int>` | summary `media.rtcp_max_jitter_ts` (when present); per-call `media.RTCPMaxJitter` | `<= threshold` (RTP TS units!) |
| `-health_min_rtp_packets_recv <n>` | summary `media.rtp_packets_received` | `>= threshold` |
| `-health_min_rtp_packets_recv_per_call <n>` | computed across per-call records' `media.RTPPacketsReceived` (min) | `>= threshold` |

**Note** — `-health_max_rtcp_jitter_ts` takes RTP timestamp units, not ms. When proposing it, multiply your ms target by the codec sample-rate divisor (e.g. 30 ms × 8 = 240 for PCMU). State the conversion in the proposal.

If `-health_*` was NOT set on the run, apply thresholds post-hoc against raw counters — same logic, in the findings report.

## Exit code interpretation

| Exit | Means | What to do |
|---|---|---|
| `0` | Clean run, no health breach (or `-health_*` not set) | Read summary JSON. Check no false-positive successes — `cancelled` mid-run shouldn't count as benign. |
| `1` | Engine / parse / transport / scenario error before finalization | Summary JSON may be missing. Read `*_err.log` first. Hand back to build (XML problem) or run (flag/transport problem). |
| `2` | Health threshold breach | Summary JSON has `health` + `findings`. Lead with which dimension breached and by how much. |

## Always-remember gotchas

1. **RTCP jitter fields are RTP timestamp units, not ms.** Convert using the codec clock. Always declare the codec assumption. Field names: per-call PascalCase (`media.RTCPMaxJitter` etc.); summary snake_case with `_ts` suffix (`media.rtcp_max_jitter_ts` etc.).
2. **`dropped_logs > 0` in OTLP `engine.stopped` events** signals OTLP buffer overflow; treat OTLP stream as incomplete. The summary JSON does NOT carry an equivalent `engine.stopped.dropped_logs` field directly — this is an OTLP-only signal. Check the OTLP collector side, not the summary.
3. **`cancelled` is timing-dependent.** End-of-soak SIGINT or `-timeout_global` produces benign cancellations. Mid-run cancellation in a per-call `error` string is a real failure. Inspect timestamps before concluding.
4. **`unexpected_sip` followed by `_unexp.main` recovery may still classify as `success`.** The fallback exists to recover. Don't flag every `_unexp.main` jump as a failure. (Per-call records that recovered will have `success: true`.)
5. **No packet-level RTP/RTCP in HEP.** For per-packet analysis (loss bursts, codec-level issues), the user needs local PCAP via heplify on the gossipper host — not the HEP mirror. Tell them.
6. **`gossipper_call_record_v1` field list is enumerated** in `references/output-schema-reference.md`. Be defensive about field presence — failure-mode records omit some media subfield meaningful values (they're present but zeroed). If `schema_version` differs from `gossipper_call_record_v1`, follow the drift-detection recipe in the schema reference.
7. **TLS handshake failures show in `error` strings** containing transport-level wording (e.g. `tls handshake`, `connection refused`). Look in `*_err.log` and per-call `error`, not in `auth` events. Failures are NOT pre-classified — derive class from the error text.
8. **Summary JSON exists only after clean shutdown.** SIGKILL → no summary. SIGINT → drain + summary write. If summary is missing on exit 1, that's why.
9. **`max_failed_calls: 0` means "any failure fails health"** — strict pass/fail gate, not "unlimited failures allowed."
10. **Built-in `-sn uac/uas` scenarios don't emit named `rtd` timers** by default — only `invite_rtt` and `call_length`. Custom timers require custom XML.
11. **Summary latency values are in nanoseconds.** `average_call_latency`, `average_invite_rtt`, and `count.average`/`min`/`max`/`last`/`stddev` inside `invite_rtt` / `call_length` objects are ns — divide by 1_000_000 for ms. Always disclose units in the findings report.

## Going deeper

Read on demand:

- **`references/output-schema-reference.md`** — full schema lookup: output channel matrix, `gossipper_summary_v1` field reference (top-level + `latencies` + `counters` + `media` + `health` + `findings`), `gossipper_call_record_v1` known shape + inventory recipe, three contracted CSV headers verbatim, three non-contracted CSVs, OTLP `Kind` enum with attrs, HEP3 mirror semantics, live API surfaces.
- **`references/patterns.md`** — how-to-act: per-class failure triage, codec → ms conversion details, health threshold authoring by run intent (SOC / soak / regression / smoke) with sample sets, correlation strategy (Call-ID selection + time-window inference), findings-report filling notes, `jq`/`awk` recipes for common extractions, data-quality flag handling, rerun-vs-escalate decision patterns.

## Verdict vocabulary

`Verdict:` in the findings report below is one of three values:

| Verdict | Meaning |
|---|---|
| `pass` | Exit code 0 AND no post-hoc threshold breach. The run is good. |
| `breach` | Exit code 2 (a `-health_*` threshold was set and breached at runtime) OR exit code 0 with a post-hoc threshold violation surfaced during analysis. |
| `hard_error` | Exit code 1 — engine/parse/transport/scenario failure. Summary JSON may be missing. |

Soft signals (data-quality, codec assumptions, single-stat outliers) do not change the verdict — they go in the `concerns[]` array. See "Concerns schema" section below.

## Findings report (handoff)

After analysis, surface this structured block in chat. Symmetric with gossipper-run's "Run report" — chat content, not a file artifact. The caller (human or autonomous controller) acts on it.

```
Findings report
---------------
Run identifier:    <self_tag / hep_capture_id>
Verdict:           pass | breach | hard_error

Inputs:
  codec_assumption: <e.g. PCMU/8000 (assumed for built-in scenario)>

Failure breakdown (derived from per-call error parsing):
  timeout: <n>  unexpected_sip: <n>  transport_error: <n>
  parse_error: <n>  scenario_error: <n>  cancelled: <n>
  (unclassified: <n>)

Latency (all values in ms, converted from ns):
  invite_rtt:   avg=<>ms  max=<>ms
  call_length:  avg=<>ms
  custom_rtds:  <name>=avg=<>ms ...

Media (if applicable; codec: <>):
  rtcp_max_fraction_lost:    <0..1>
  rtcp_max_jitter_ms:        <> (converted from <> RTP TS units)
  per_call_min_rtp_packets:  <>

Health (if -health_* was set, or applied post-hoc):
  passed:    <bool>
  breached:  <thresholds with actual vs target>

Correlation pointers (for HOMER/qryn follow-up):
  hep_capture_id:   <int>
  self_tag:         <id>
  time_window:      <start ISO 8601> .. <end ISO 8601>
  key_call_ids:     <failed-call IDs, capped at N>

Threshold proposals for next run (if any):
  -health_min_success_ratio    <val>
  -health_max_timeouts         <val>
  ...

Concerns (see "Concerns schema" section; may be empty):
  - code: <code>  severity: <info|notice>  message: <one-line>

Recommended next step:
  <rerun with X via gossipper-run | scenario change Y via gossipper-build |
   escalate: production-side HOMER/qryn correlation needed>
```

Do NOT paste raw summary JSON alongside this block — the report is the synthesis, not a wrapper. Paths live in the Run report; the caller re-opens them if needed.

## Concerns schema

Verdict is `pass | breach | hard_error`. **Concerns** carry soft signal that doesn't change the verdict but a reviewer should see. Always emit as a top-level `concerns[]` array in the structured findings (alongside the chat-format report above); empty array is fine. The chat-format report's "Concerns" section enumerates them one bullet per entry.

### Shape (structured findings)

```json
{
  "verdict": "pass",
  "inputs": {
    "self_tag": "...",
    "codec_assumption": "PCMU/8000"
  },
  "concerns": [
    {
      "code": "jitter_variance_high",
      "severity": "info",
      "message": "Jitter mean 8ms (within 30ms budget) but stddev 12ms — high variance",
      "metric": { "name": "jitter_stddev_ms", "value": 12, "threshold": null }
    }
  ]
}
```

`severity` is one of `info | notice`. Anything that should fail a run goes into `verdict: breach`, not `concerns[]`.

### Trigger codes (initial set)

| Code | Trigger condition | Severity | Example metric |
|---|---|---|---|
| `jitter_variance_high` | Jitter mean within budget, but stddev > 50% of mean | `info` | `jitter_stddev_ms` |
| `retransmit_present` | Summary `retransmits > 0`, even if calls completed successfully | `info` | `retransmits` |
| `codec_assumed` | Codec inferred via the PCMU default rule for `-sn uac/uas` runs | `info` | n/a |
| `partial_data` | Summary JSON missing expected fields (`findings[]` empty, summary missing entirely on exit 1, etc.) | `notice` | depends |
| `unclassified_failures` | Per-call records contain `success: false` with `error` strings not matching any class regex | `notice` | `unclassified_count` |

Add new codes only when a recurring case warrants one — don't proliferate single-use codes.

### When to emit which

- `pass` + non-empty `concerns[]` → "passed, but reviewer should see X." Common case.
- `breach` + `concerns[]` → "failed on threshold Y, also worth noting Z." Concerns are additive, not redundant with breach detail.
- `hard_error` + `concerns[]` → rare but valid; e.g. "summary missing — verdict provisional based on exit code alone."

