# Investigation Agent (A6) — Design

**Status:** Draft for review
**Parent doc:** [Proposal_arch.md](Proposal_arch.md)
**Re-homes:** [investigation_agent_design_copied.md](investigation_agent_design_copied.md) (the mw-core version)
**Position in roster:** A6 — read-only, cross-domain, on-demand. **Built first** (Phase 1).

> The inherited executive summary ends with *"It is going to extend for agent Investigation"*. The mw-core document is that extension, written for a system where the necessary data was spread across four services. This document re-homes it to TMS, where it is not.

## 0. What changed from the mw-core version

The mw-core design is sound and most of it survives the move. Three things carry over unchanged, one collapses, and two are new.

**Carried over:**
- **Read-only, enforced in code.** mw-core marks each tool `access: :read` in the registry and rejects non-read tools before invocation, *"not trusted to the prompt."* Kept exactly — see §6.
- **Bounded step budget.** Prevents runaway loops and caps cost; budget exhaustion returns partial findings marked inconclusive rather than guessing. Kept, with per-round and per-user bounds added.
- **Evidence, not just prose.** Return raw tool results alongside the narrative so the operator can verify — *"never 'trust the summary'."* Kept, and hardened: every claim in the report cites an evidence ID, and an uncited claim is treated as malformed output.

**Collapsed — the correlation key (mw-core §5, its largest work-stream):**

mw-core needed an end-to-end trace ID stamped at ingress and carried through switch → settlement → payout, or a lineage service mapping between each system's native key. It called this *"usually the largest and most cross-team item"* and made phases 2–3 depend on it.

**That work is already done in this repo.** `settlement_core` holds the whole chain in one MySQL database behind one Repo, with the correlation materialised as columns and foreign keys:

| Hop | mw-core | Here |
|---|---|---|
| Switch auth | External Switch, keyed by RRN/STAN — **needs a new read API** | `switch_dump_records` — `rrn`, `tid`, `auth_number`, `stan`, `response_code`, plus `match_status` / `matched_at` / **`core_transaction_id`** already resolved by `ReconciliationEngine` |
| Core txn | `transactions` shadow copy keyed by `cbs_reference` | `core_transactions` — `rrn`, `tid`, `auth_number`, `stan`, `invoice_number`, `batch_number`, `merchant_mid`, `settlement_batch_id`, `settlement_status` |
| Settlement | External Settlement system, batch reference — **needs a new read API** | `settlement_mis_items` — **`core_transaction_id` FK**, `settlement_mis_id`, plus its own `rrn`/`tid`/`auth_number` |
| Payout | External Payout system, disbursement id — **needs a new read API** | `payout_items` — `settlement_mis_id`, `payout_batch_id`, `status`, `bank_transfer_ref` → `payout_batches` → bank confirmation |
| Why it failed | Must be inferred | **`reconciliation_exceptions`** — `exception_type`, `sla_due_at`, `status`, with FKs to the core transaction *and* both dump-record types |

So mw-core's §6 ("required changes per system": three external teams each building a read interface) reduces here to **writing Ecto queries**. Its worked example in §7 — five sequential cross-service tool calls — becomes a handful of joins, and the last one often isn't needed because the exception row already names the cause.

**New here:**
- **Fleet/config/OTA domains.** mw-core covered transactions only. TMS adds terminal estate, config compliance, and rollouts — the "why did 60 terminals drop at once?" class of question.
- **Agent self-inspection.** A6 reads `agent_decisions` / `agent_actions` / `agent_outcomes` and explains the *other* agents' behaviour. mw-core had no acting agents to debug; this system will.

**One provider note.** mw-core was constrained to OpenAI (*"the only provider actually wired today"*), and its `LlmConfig.call_with_tools/3` was scoped to OpenAI's function-calling API. Nothing is wired in this repo — a scan of `mix.lock` and every `.ex`/`.exs` finds no LLM provider — so the choice is open. The recommendation is Claude via the Messages API tool-use loop; see [Proposal_arch.md](Proposal_arch.md) D6 for the exact request shape and the constraints that are easy to get wrong.

---

## 1. What it is, and what it deliberately is not

The Investigation Agent answers **"what actually happened, and why?"** It does not fix anything.

| | |
|---|---|
| **Is** | A read-only evidence-gathering and root-cause-narrative agent, spanning terminal / config / OTA / settlement / risk domains |
| **Is not** | An actuator. It holds **zero T1+ tools**. Its entire tool catalogue is read-only queries. |
| **Invoked by** | (a) An operator asking a free-text question in the console; (b) `A7 Ops Orchestrator` when an incident is novel or cross-domain; (c) a post-incident review job after any incident closes as `worsened` or `no_effect` |
| **Produces** | An **Investigation Report** — a cited, structured artefact persisted to `agent_investigations` |

### Why read-only is the right constraint

The other six agents are narrow and act. This one is broad and does not. That separation is the safety property:

- Breadth of data access and power to act are the two things you never want in the same component.
- It can be given generous read access across all five databases/domains precisely *because* it cannot do anything with it.
- It can be run by any operator, on any question, at any time, without an approval gate — the worst case is a wasted API call and a wrong narrative that a human reads and discards.
- It is the natural place to put the **most capable / highest-cost model configuration**, since invocation is human-paced rather than event-paced.

---

## 2. Where investigation is needed today

Grounded in real gaps in this repo:

| Question an operator asks now | What they do today | What A6 does |
|---|---|---|
| **"Txn X shows approved in the switch, but the merchant says it's missing from their settlement report. Where did it stop?"** | Query `switch_dump_records`, then `core_transactions`, then MIS, then payout — by hand, per dispute | Walks the §0 join path and reports the first hop where the chain breaks, with the `reconciliation_exceptions` row if one exists. **This is mw-core's flagship scenario, and here it's a query rather than a cross-team programme.** |
| "Why did 60 terminals in Area 5 go offline last Tuesday 14:00–15:30?" | Manually cross-reference `tms_terminal_status_logs`, guess at ISP vs config vs power | Correlates the offline window against pushes, rollouts, group-rule changes, and merchant/area topology; reports what co-occurred |
| "Why does SN 98250623730001 keep failing its EMV config push?" | Read `parameter_push_logs` rows by hand, pull remote logs, compare checksums | Assembles push history, ACK/NAK payloads, checksum vs artefact on disk, firmware band, and prior identical fingerprints |
| "Recon SLA breached at 06:15 — what broke?" | `SlaMonitorWorker` sends an email that says only "not completed by 06:15" | Traces backwards: dump file arrival time → parse errors → Oban job failures → exception volume spike, and names the first divergence |
| "Rollout of app v2.3.1 is at 12% failure — is it the package or the devices?" | Eyeball `application_upgrade_status_live` | Segments failures by vendor/model/firmware/area and reports whether failure is correlated with a device attribute or uniform (⇒ package fault) |
| "Agent A2 kept re-pushing to this terminal 9 times. Why?" | No mechanism exists | Reads `agent_decisions` / `agent_actions` / `agent_outcomes` and explains the agent's own loop — **self-inspection** |

That last row matters: **A6 is the debugger for the rest of the agent system.** Without it, `agent_decisions` is a table nobody reads.

---

## 3. Tool catalogue (all read-only)

Every tool is scoped, parameterised, and returns bounded result sets. No raw SQL, ever.

### Terminal & fleet
```
get_terminal(serial_number)                       → tms_terminals row + derived state
get_terminal_status_history(sn, from, to)         → tms_terminal_status_logs (capped 500)
get_status_items(sn, from, to)                    → tms_terminal_status_items
find_terminals(filters)                           → by vendor/model/area/merchant/group/tier
get_fleet_snapshot(from, to, group_by)            → online/offline counts bucketed
get_heartbeat_gaps(sn | cohort, from, to)         → derived gap intervals
```

### Config & push
```
get_push_history(sn | template_id, from, to)      → parameter_push_logs + push_details
get_push_outcome_stats(filters)                   → success/fail rates sliced by any dimension
get_config_versions(vendor, model)                → config_file_versions + parameter_templates
get_artifact_metadata(path)                       → size, checksum, mtime for priv/ota artefacts
                                                     (metadata only — never file contents)
diff_versions(config_type, v1, v2)                → structured diff of two template versions
```

### OTA & rollout
```
get_rollout_status(package_id | config_id)        → app_upgrade_device_status aggregate
get_device_upgrade_history(sn)                    → per-device upgrade timeline
segment_rollout_failures(package_id, dimension)   → failure rate by vendor|model|firmware|area
```

### Settlement & transaction lineage
```
trace_transaction(rrn | tid+auth_number | core_transaction_id)
      → THE JOIN-PATH WALKER. One call, one result set, the whole chain:
          pos_transaction / pos_failed_transaction / pos_transaction_reversal
            (LIVE jPOS switch record — same DB, same-request freshness;
             s_tid_stan, reference_no (RRN), acquirer_reference_no,
             approval_code, response_code — see Proposal_arch.md §1.6)
            ↓ (fallback only if absent from the live table)
          switch_dump_records / qr_scheme_dump_records  (EOD dump mirror, response_code, match_status)
            → core_transactions        (settlement_status, settlement_batch_id, risk_hold)
              → settlement_mis_items   (which MIS, what amounts)
                → payout_items         (payout_batch_id, status, bank_transfer_ref)
                  → payout_batches     (transmission + bank confirmation)
        plus any reconciliation_exceptions row keyed to the same transaction.
        Returns each hop as present / absent / mismatched, so the FIRST BREAK
        is a structural fact in the result — not something the model infers.

get_settlement_timeline(date)                     → dump arrival → recon → MIS → approval
get_recon_exceptions(date, filters)               → reconciliation_exceptions (capped)
get_exception_clusters(from, to)                  → grouped by type/merchant/dump_source
get_sla_checkpoint_history(from, to)              → SlaMonitorWorker checkpoint results
get_merchant_settlement_summary(mid, date_range)  → what the merchant should have received
```

`trace_transaction/1` is the highest-value tool in the catalogue and should be built first. Three design points:

- **It is deterministic Elixir, not model reasoning.** The walker resolves each hop with a query and labels it `:present | :absent | :mismatched`. The model's job is to *explain* the first break and check it against precedent — not to work out the join path itself. This keeps the flagship scenario correct even when the model is uncertain, and makes it unit-testable against known-good and known-broken transactions.
- **It accepts several entry keys** because disputes arrive with whatever the merchant quoted — an RRN, a TID + auth number, sometimes only an invoice number and a date.
- **It checks the live switch table before the EOD dump mirror.** `pos_transaction` (and its `pos_failed_transaction`/`pos_transaction_reversal` siblings) is the jPOS switch's own record, written directly into the same `shukria_transactions` database this repo already reads (`DaProductApp.PosTransactions.PosTransaction` — see Proposal_arch.md §1.6). It is fresher than `switch_dump_records`, which only fills in after end-of-day dump ingestion. Query it first; fall back to the dump tables only for fields the live schema doesn't carry.

### Merchant risk & compliance (read-only, via the verified `SettlementCore.Mms.*` path)

**Correction from an earlier draft:** these tools were originally specced as direct queries against `ShukriaMmsRepo` using table/field names sourced from the Cloud Layer RFP doc (including a `merchant_configuration` table that doesn't exist). Verified against the actual code instead — see [entity-resolution.md](entity-resolution.md) §2.1. The real, tested resolution path is `SettlementCore.CoreTransactionSync.get_merchant_metadata/1`, keyed on `merchant_refrence_number` (the real column name, misspelling and all). These tools should call through that existing function, not reinvent the query:

```
get_merchant_risk_profile(mid | merchant_reference_number)
      → SettlementCore.Mms.MerchantMetadata (table: user_metadata) + MmsUser
        (table: users) — mc_code, mcc_id, contract_approval_status, group_code.
        Resolved via SettlementCore.CoreTransactionSync.get_merchant_metadata/1,
        the same function core transaction sync already uses — not a new query.
        READ-ONLY: PlatformCore.ShukriaMmsRepo is `read_only: true` at the Ecto
        adapter level (verified, entity-resolution.md §2.1) — Ecto itself refuses
        any non-SELECT statement through this Repo. No tool should attempt a
        write regardless; this is belt-and-suspenders on top of a real guarantee.

get_kyc_status(mid)                               → SettlementCore.Mms.KycRequest
                                                     (table: kyc_requests): completion %,
                                                     per-method approval status
get_merchant_mdr_profile(mid)                     → SettlementCore.Mms.MdrTemplate /
                                                     MdrRate / MccMdrRate — active MDR,
                                                     MCC risk tier via Mcc/CardType
```

*(LSEG/World-Check screening data was speculatively included in an earlier draft — no `SettlementCore.Mms.*` schema for it was found during verification. If epay-main's LSEG results live in a table this grep didn't reach, add it back once confirmed; don't wire a tool against an unverified table.)*

These exist to answer the question a settlement-only investigation can't: *is this merchant's account itself the anomaly?* An investigation that finds "transaction never reached the MIS" is more complete when it can also report "and this merchant's contract approval is still pending" — context that changes whether the right next step is a settlement fix or a compliance escalation.

### Risk
```
get_risk_hits(filters)                            → risk_rule_hits (REDACTED — see §6)
get_risk_rule_stats(rule_id, from, to)            → hit volume + disposition breakdown
```

### Jobs & platform
```
get_oban_failures(queue, from, to)                → failed/retry-exhausted jobs + error
get_mqtt_event_log(topic_pattern, from, to)       → observed MQTT events (agent_observations)
get_infra_health(from, to)                        → OPTIONAL Prometheus passthrough (§9 parent doc)
```

### Agent self-inspection
```
get_incident(incident_id)                         → full incident with observations
get_agent_decisions(filters)                      → agent_decisions traces
get_agent_actions(incident_id | tool | fingerprint)
get_outcome_stats(agent, fingerprint, from, to)   → success rates
find_similar_incidents(fingerprint, limit)        → precedent retrieval
```

**Enforced invariants**
- Every tool has a **hard row cap** and a **mandatory time window** (max 90 days).
- No tool returns file contents, key material, PANs, or credentials.
- No tool writes. The module has no `execute/2` write path at all — reads go through a separate `AgentCore.ReadTool` behaviour with no counterpart to `AgentCore.Tool.execute/2`.

---

## 4. Investigation loop

Distinct from the parent control loop — this one is **iterative and hypothesis-driven**, and terminates on budget, not on action.

```
 Trigger: operator question | orchestrator escalation | post-incident review
        │
        ▼
 [1] SCOPE          Parse into a structured investigation frame:
        │             subject (terminal|cohort|rollout|date|incident|agent)
        │             time window (explicit, or inferred and STATED)
        │             question type (why-failed | why-offline | why-slow |
        │                            what-changed | is-it-systemic | self-audit)
        │           Ambiguous scope → ASK THE OPERATOR, do not guess.
        ▼
 [2] ORIENT         Cheap deterministic pre-fetch (no LLM):
        │             subject record, recent state, obvious counters.
        │           Seeds the context so turn 1 is not wasted on lookups.
        ▼
 [3] HYPOTHESISE    Model proposes 2–5 candidate explanations, each with
        │           a discriminating test naming the specific tool call
        │           that would confirm or refute it.
        │           ── Requiring a discriminating test per hypothesis is what
        │              stops it from confabulating a plausible story. ──
        ▼
 [4] GATHER         Execute the discriminating reads (parallel where independent).
        │           Every result appended to the evidence ledger with
        │           tool name + params + row count + timestamp.
        ▼
 [5] ELIMINATE      Refute or support each hypothesis AGAINST CITED EVIDENCE.
        │           Surviving hypotheses may spawn refined sub-hypotheses.
        │           Loop back to [3] — bounded (default: 4 rounds, ~25 tool calls).
        ▼
 [6] CONCLUDE       Emit the Investigation Report (§5).
        │           If evidence is insufficient: SAY SO. "Undetermined, and here
        │           is what would settle it" is a valid and valuable outcome.
        ▼
 [7] PERSIST        → agent_investigations. Linked to the incident if there is one.
                    Operator rates it (useful / partly / wrong) → feeds calibration.
```

**Budget discipline.** Broad read access plus an iterative loop is how you accidentally spend a lot of money. Bounds: max rounds (4), max tool calls (25), max total tokens per investigation, and a per-user daily investigation quota. Exceeding any bound produces a **partial report clearly labelled as budget-truncated**, never a silent stop.

---

## 5. Investigation Report format

Structured, not prose-blob. Rendered as a LiveView card; exportable as Markdown for incident reviews.

```
INVESTIGATION #1247
Question     : Why did 60 terminals in Area 5 go offline 2026-07-14 14:00–15:30?
Scope        : cohort · area=5 · 2026-07-14T13:00Z → 2026-07-14T17:00Z
Requested by : ops.hassan · Triggered: manual
Model        : claude-opus-4-8 · 18 tool calls · 3 rounds · 42s · $0.31

── FINDING ────────────────────────────────────────────────────────────
Most likely cause (confidence: MEDIUM-HIGH)
  A parameter template push (template_id=88, v1.2.0) was issued to 74
  Area-5 terminals at 13:58Z. 60 of the 62 that ACKed went offline within
  9 minutes of ACK. The 12 that never ACKed stayed online.

  The correlation is with SUCCESSFUL config application, not with the
  push attempt — which points at the template content, not delivery.

── EVIDENCE ───────────────────────────────────────────────────────────
E1  74 pushes, template 88 v1.2.0, 13:58:02–13:58:47Z
      → get_push_history(template_id: 88, ...) · 74 rows
E2  62 ACKs 13:59–14:04Z; 12 no ACK
      → get_push_history(...) · status breakdown
E3  Offline transitions: 60, all 14:03–14:09Z, all in the ACK set
      → get_terminal_status_history(cohort: area=5, ...) · 60 rows
E4  All 12 non-ACK terminals still reporting heartbeats at 17:00Z
      → get_fleet_snapshot(...) · 12 rows
E5  Template 88 v1.1.0 → v1.2.0 changed `comm_timeout` 30s → 3s
      → diff_versions("parameter", "1.1.0", "1.2.0")
E6  No rollout, no group-rule change, no Oban failures in the window
      → get_rollout_status(...), get_oban_failures(...) · 0 rows

── HYPOTHESES CONSIDERED ──────────────────────────────────────────────
✓ SUPPORTED  Bad template value applied on ACK       [E1,E2,E3,E4,E5]
✗ REFUTED    Regional connectivity/ISP outage        [E4 — non-ACK
                terminals in the same area stayed up]
✗ REFUTED    Concurrent app rollout                  [E6 — none active]
? UNTESTED   Power event at merchant sites           [no data source
                available in TMS — would need merchant confirmation]

── WHAT WOULD CONFIRM ─────────────────────────────────────────────────
  · Pull remote logs from any Area-5 terminal that recovers
    (RemoteLogService — requires an operator, A6 cannot act)
  · Check whether the same template reached any non-Area-5 terminal
    and whether those also dropped

── SUGGESTED NEXT ACTIONS (for a human — A6 executes nothing) ─────────
  1. Quarantine template 88 v1.2.0                      [T3 — approval]
  2. Re-push v1.1.0 to affected cohort                  [T2 — approval]
  3. Root-cause the 3s comm_timeout value with whoever authored it

── LIMITATIONS ────────────────────────────────────────────────────────
  Correlational, not causal. No terminal-side logs available for the
  window (devices offline). Merchant power/network status unavailable.
```

### Non-negotiable properties of the format

1. **Every claim cites evidence IDs.** A statement without a citation is a bug, and the report renderer flags it visually.
2. **Refuted hypotheses are shown.** Knowing what was ruled out, and on what basis, is most of the value in a review.
3. **`UNTESTED` is a first-class outcome** — it names the limits of TMS's data, which is itself an actionable finding.
4. **Suggested actions carry their risk tier** and are explicitly marked as human-executed.
5. **Limitations are mandatory.** A report with no limitations section is treated as malformed.

### Worked example — the merchant dispute (mw-core's §7, re-homed)

This is the scenario mw-core was designed around. Its version needed five sequential cross-service calls to systems that didn't yet expose read APIs. Here:

```mermaid
sequenceDiagram
    participant U as Operator (console)
    participant A as InvestigationAgent
    participant T as trace_transaction/1
    participant DB as PlatformCore.Repo (one DB)

    U->>A: "Txn RRN 4471… approved in switch, merchant says missing. Investigate."
    A->>T: trace_transaction(rrn: "4471…")
    T->>DB: switch_dump_records → core_transactions → settlement_mis_items → payout_items
    T->>DB: reconciliation_exceptions by rrn / core_transaction_id
    DB-->>T: 4 hops + 1 exception row
    T-->>A: switch: PRESENT (response_code 00, match_status matched)
    T-->>A: core_transaction: PRESENT (settlement_status "unmatched", risk_hold false)
    T-->>A: settlement_mis_item: ABSENT  ← first break
    T-->>A: payout_item: ABSENT (consequent)
    T-->>A: exception: type "late_presentment", sla_due_at …, status "hold"
    Note over A: First break located structurally, not inferred.<br/>Agent explains WHY and checks precedent.
    A->>A: find_similar_incidents(fingerprint: late_presentment + this merchant)
    A-->>U: Auth confirmed at switch (RC 00, matched). Transaction never entered<br/>the MIS: held as a late_presentment exception on <date>, so it never<br/>reached payout and is correctly absent from the merchant's report.<br/>14 similar exceptions for this MID in 30 days — pattern, not one-off.<br/>Suggested: finance reviews the hold [T4 — human only]. Evidence attached.
```

Two things this makes concrete:

- **The value is correlation + narrative + evidence, not any single lookup** — mw-core's own framing, and still right. What changed is that the correlation is a query instead of an integration programme.
- **The agent does not release the hold.** `ReconciliationEngine.release_transaction/1` is T4 — no tool wraps it. A6 assembles the packet and routes it to the existing finance queue.

---

## 6. Safety

A6 has broader data reach than any other agent, so the controls are about **exposure**, not blast radius.

| Concern | Control |
|---|---|
| **Cardholder / PII exposure** | `get_risk_hits` and all settlement tools pass through a mandatory redaction layer: PAN → first6+last4, no CVV, no track data, no full merchant bank details. This now explicitly covers `pos_transaction`'s `encrypted_pan`, `hash_pan`, `encrypted_track2`, and `encrypted_expiry` fields — `trace_transaction/1` must never return them, only `masked_card_no`. Redaction is a unit-tested pure function applied at the tool boundary, not a prompt instruction. |
| **Key material** | No tool reads `keys.json`, KEK/KCV, slot data, or RKI responses. `get_artifact_metadata` returns metadata only and explicitly refuses paths under the keys directory. |
| **`ShukriaMmsRepo` is read-only** | Verified stronger than originally stated: `apps/platform_core/lib/platform_core/shukria_mms_repo.ex` declares `read_only: true` at the Ecto adapter level, not just a config comment — Ecto itself refuses any non-`SELECT` statement through this Repo (entity-resolution.md §2.1). `AgentCore.ReadTool` should still refuse to register a write-shaped query against it as defense in depth, but the underlying guarantee doesn't depend on that check. |
| **File contents** | No tool returns file bodies. Checksums and sizes only. |
| **Prompt injection via device data** | Remote log payloads and `remark` fields are device-controlled. They are wrapped in explicit data delimiters and prefixed as untrusted. Because A6 has **no actuators**, a successful injection can at worst produce a misleading report — it cannot cause an action. This is the strongest single argument for keeping A6 read-only. |
| **RBAC** | Investigation results inherit the requesting operator's data scope. A merchant-scoped operator cannot investigate another merchant's terminals — enforced by injecting scope filters into every tool call server-side, not by asking the model to respect them. |
| **Cost abuse** | Per-user daily quota; per-investigation token ceiling; expensive tools (fleet-wide scans) rate-limited independently. |
| **False confidence** | Confidence is a required, enumerated field (`LOW | MEDIUM | MEDIUM-HIGH | HIGH`). Reports below `MEDIUM` render with a prominent caveat banner. Operator ratings feed a calibration report reviewed monthly. |
| **Auditability** | Full evidence ledger persisted — every tool call, params, row counts. A report is reproducible: re-running the same ledger must yield the same evidence. |

---

## 7. Data model addition

```
agent_investigations
  id, investigation_ref, question (text), scope (json),
  trigger ('manual'|'orchestrator'|'post_incident'),
  requested_by_id, incident_id (nullable),
  status ('running'|'complete'|'budget_truncated'|'failed'),
  finding (text), confidence ('low'|'medium'|'medium_high'|'high'),
  hypotheses (json),        -- [{statement, verdict, evidence_ids}]
  evidence (json),          -- [{id, tool, params, row_count, summary, at}]
  suggested_actions (json), -- [{description, risk_tier}]
  limitations (text),
  model_id, prompt_version, rounds, tool_call_count,
  input_tokens, output_tokens, cost_cents, duration_ms,
  operator_rating ('useful'|'partly'|'wrong'), operator_notes,
  inserted_at
```

---

## 8. Console integration

New LiveView at `/agents/investigations`:

- **Ask** — free-text box with scope pickers (subject type, time window). Streams the loop live: hypotheses appear, then get struck through as evidence refutes them. This visible reasoning is what builds operator trust faster than any accuracy number.
- **Report view** — the §5 card, with every evidence ID expandable to the raw tool result.
- **From anywhere** — an "Investigate" button on terminal detail, rollout status, incident cards, and SLA breach alerts, pre-filling scope.
- **Rate** — useful / partly / wrong, plus free-text. Two clicks, or it will not get used.
- **History** — searchable past investigations; identical scope within 1h offers the cached prior report before spending tokens.

---

## 9. Build sequence

**A6 is Phase 1 of the parent plan** — first, ahead of every acting agent. This follows mw-core's own sequencing argument (read-only, low blast radius, no approval gates, and it exercises the tool-calling runtime everything else needs), which holds here and is *strengthened* by the correlation key already existing.

Phase 0 prerequisites, built once and shared with every later agent:
- `AgentCore.LLM` — the Messages API tool-use loop with a bounded step budget
- `AgentCore.ReadTool` behaviour + the redaction layer at the tool boundary
- RBAC scope injection (server-side, never asked of the model)

Phase 1 build: ~4–6 weeks.
- Week 1: `trace_transaction/1` — the join-path walker, deterministic, unit-tested against known-good and known-broken transactions
- Week 2: remaining read tools across terminal / config / OTA / risk, with row caps and time windows
- Week 3: investigation loop + budget enforcement + hypothesis/discriminating-test structure
- Week 4: report generation, persistence, `/agents/investigations` console
- Weeks 5–6: self-inspection tools (needed for Phase 2's shadow review) + evaluation

**Acceptance criteria** — both must hold:

1. On 20 past incidents with known root causes (mixed: disputes, offline cohorts, failed rollouts, SLA breaches), A6 identifies the correct primary cause in ≥ 70%.
2. **Zero confidently-wrong reports** — never rates `HIGH` on an incorrect finding. A confident wrong answer is worse than `UNDETERMINED`, and the bar should say so explicitly.

Add a third for the flagship path specifically: `trace_transaction/1` must correctly locate the first break on **100%** of a fixture set of known-broken transactions. That tool is deterministic, so anything less is a bug, not a model limitation.

---

## 10. Open questions

1. **Should A6 be able to trigger A1's remote-log tool?** It would substantially improve terminal investigations, but it breaks the read-only property. **Recommendation: no** — instead it emits a suggested action that an operator executes in one click from the report. Keeps the invariant, costs one click.
2. **Retention** for `agent_investigations` — these contain redacted operational detail; propose 12 months, aligned with whatever `risk_audit_logs` retention is set to.
3. **Should the orchestrator auto-invoke A6 on every novel incident,** or only on escalation? Auto-invoke gives better incident records but multiplies cost. Suggest: auto-invoke only on `severity >= high` or on any incident that closes `worsened`.
4. **Merchant-side data sources** (power, network) do not exist in TMS. Several investigations will terminate at `UNTESTED` for this reason. Worth deciding whether that gap is worth closing.
