It is 2:47 AM. A PagerDuty notification cuts through the silence: HTTP 5XX rate elevated on the order service. The engineer reaches for their laptop, pulls up Grafana, and begins the familiar ritual of scanning dashboards, cross-referencing logs, forming a theory, testing it, and restarting a pod. By 3:15 AM, the problem is gone, and the engineer is back in bed, none the wiser about what actually caused it.
This is the lived reality of an on-call engineer in a microservices world.
The tooling isn't broken; Prometheus, Grafana, and AlertManager are excellent at what they do. But what they do is collect and visualize signals. Interpreting those signals, reasoning about the root cause, and deciding what to do still falls entirely on the engineer at an inconvenient hour.
So I built an agent on a different premise: what if the system that detects an anomaly could also diagnose it, decide on a remediation, act on it, and deliver a complete incident report before anyone's phone rings?
This walkthrough shows exactly how it's built – Prometheus, Claude, Kubernetes, and Slack, wired into a closed loop so you can follow the same pattern in your own environment. Each step is a piece of that loop.
Step 1: Map the System You're Protecting
Before the agent can heal anything, you have to know what it's watching, so the first step is to map the services you want it to cover.
The stack below is the example used throughout this walkthrough: a representative e-commerce backend of five services. It isn't a template you have to copy; your own system will have different services, languages, and ports. What carries over to any architecture is the principle at the end of this section.
Nginx API Gateway: The single public entry point
Order Service (Node.js): Order creation and retrieval against PostgreSQL
Inventory Service (Python/FastAPI): Real-time stock levels
Notify Service (Node.js): Outbound Slack and email
AI Agent Service (Python): The fifth service, the one that watches the other four

Figure 1: A high-level architecture diagram that shows the complete closed-loop system developed throughout this walkthrough.
User traffic (blue) enters through the Nginx gateway and is routed to four application services, each exposing the/health and /metrics endpoints. Prometheus collects these metrics every 15 seconds (orange), and Grafana provides visualisation.
The self-healing loop is situated on the right: the AI Agent Service executes a 60-second monitoring cycle, querying Prometheus, forwarding anomalies to Claude (Sonnet 4.6) for diagnosis, and, when necessary, invoking the Kubernetes API to restart or scale the affected service (red, representing the control and remediation path).
Each cycle generates an incident report that is sent to Slack (purple) and records an entry in the PostgreSQL incidents database (green). The legend at the bottom associates each arrow color with a specific flow type, enabling tracing of individual paths: traffic, metrics, data, alert, or remediation – from origin to destination.
The mix of Node.js and Python is deliberate: most real teams are polyglot by necessity, not design. Services talk over Kubernetes DNS, share a PostgreSQL database, and deploy independently. Critically, every service exposes a /health endpoint for Kubernetes probes and a /metrics endpoint for Prometheus; the latter is the foundation on which everything else rests.
Step 2: Instrument Every Request
The goal is to make every HTTP transaction leave a trace, which is how long it took and whether it succeeded.
Both the order and inventory services attach a middleware layer that records two metrics per request: a histogram for latency distribution and a counter for totals, both labeled by HTTP method, route, and status code.
In the order service, that's a single express middleware (prom-client) that starts a timer, waits for the response to finish, and records both. The inventory service mirrors it via FastAPI's middleware interface across different languages with identical semantics.
order-service/index.js (Node.js / Express):
const express = require('express');
const { Pool } = require('pg');
const promClient = require('prom-client');
const winston = require('winston');
const app = express();
app.use(express.json());
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [new winston.transports.Console()]
});
const register = new promClient.Registry();
promClient.collectDefaultMetrics({ register });
const httpRequestDuration = new promClient.Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route', 'status'],
registers: [register]
});
const httpRequestTotal = new promClient.Counter({
name: 'http_requests_total',
help: 'Total HTTP requests',
labelNames: ['method', 'route', 'status'],
registers: [register]
});
const pool = new Pool({
host: process.env.DB_HOST || 'postgres',
port: process.env.DB_PORT || 5432,
database: process.env.DB_NAME || 'orders',
user: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || 'postgres',
ssl: process.env.DB_SSL === 'false' ? false : { rejectUnauthorized: false }
});
pool.query(`
CREATE TABLE IF NOT EXISTS orders (
id SERIAL PRIMARY KEY,
customer_name VARCHAR(255),
product VARCHAR(255),
quantity INT,
created_at TIMESTAMP DEFAULT NOW()
)
`).catch(err => logger.error('DB init error:', err));
app.use((req, res, next) => {
const end = httpRequestDuration.startTimer();
res.on('finish', () => {
end({ method: req.method, route: req.route?.path || req.path, status: res.statusCode });
httpRequestTotal.inc({ method: req.method, route: req.route?.path || req.path, status: res.statusCode });
});
next();
});
app.post('/orders', async (req, res) => {
try {
const { customer_name, product, quantity } = req.body;
const result = await pool.query(
'INSERT INTO orders (customer_name, product, quantity) VALUES ($1, $2, $3) RETURNING *',
[customer_name, product, quantity]
);
logger.info('Order created', { orderId: result.rows[0].id });
res.status(201).json(result.rows[0]);
} catch (err) {
logger.error('Order creation failed', { error: err.message });
res.status(500).json({ error: 'Internal server error' });
}
});
app.get('/orders/:id', async (req, res) => {
try {
const result = await pool.query('SELECT * FROM orders WHERE id = $1', [req.params.id]);
if (result.rows.length === 0) return res.status(404).json({ error: 'Order not found' });
res.json(result.rows[0]);
} catch (err) {
logger.error('Order fetch failed', { error: err.message });
res.status(500).json({ error: 'Internal server error' });
}
});
app.get('/health', (req, res) => {
res.json({ status: 'healthy', service: 'order-service' });
});
app.get('/metrics', async (req, res) => {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => logger.info(`Order service running on port ${PORT}`));inventory-service/main.py (Python / FastAPI):
from fastapi import FastAPI, HTTPException
from prometheus_client import Counter, Histogram, generate_latest, CONTENT_TYPE_LATEST
from fastapi.responses import Response
import logging
import os
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
app = FastAPI(title="Inventory Service")
inventory_db = {
"laptop": {"stock": 50, "price": 999.99},
"phone": {"stock": 100, "price": 699.99},
"tablet": {"stock": 75, "price": 499.99}
}
http_requests_total = Counter('http_requests_total', 'Total HTTP requests', ['method', 'endpoint', 'status'])
http_request_duration = Histogram('http_request_duration_seconds', 'HTTP request duration', ['method', 'endpoint'])
@app.middleware("http")
async def metrics_middleware(request, call_next):
with http_request_duration.labels(method=request.method, endpoint=request.url.path).time():
response = await call_next(request)
http_requests_total.labels(method=request.method, endpoint=request.url.path, status=response.status_code).inc()
return response
@app.get("/inventory/{product}")
async def get_inventory(product: str):
if product not in inventory_db:
logger.warning(f"Product not found: {product}")
raise HTTPException(status_code=404, detail="Product not found")
logger.info(f"Inventory checked for: {product}")
return inventory_db[product]
@app.post("/inventory/{product}/reduce")
async def reduce_inventory(product: str, quantity: int):
if product not in inventory_db:
raise HTTPException(status_code=404, detail="Product not found")
if inventory_db[product]["stock"] < quantity:
raise HTTPException(status_code=400, detail="Insufficient stock")
inventory_db[product]["stock"] -= quantity
logger.info(f"Reduced {quantity} units of {product}")
return {"product": product, "remaining_stock": inventory_db[product]["stock"]}
@app.get("/health")
async def health():
return {"status": "healthy", "service": "inventory-service"}
@app.get("/metrics")
async def metrics():
return Response(content=generate_latest(), media_type=CONTENT_TYPE_LATEST)
if __name__ == "__main__":
import uvicorn
port = int(os.getenv("PORT", 8000))
uvicorn.run(app, host="0.0.0.0", port=port)
This is important because this raw, labeled telemetry is the input to every query the agent will ever run. No instrumentation, no agent.
How the instrumentation works
Both services follow the same three-part pattern:
Two labelled metrics. A Histogram (http_request_duration_seconds) captures latency; a Counter (http_requests_total) counts requests. Both are labeled by method, route, and status, and those labels let you slice the data later in PromQL (e.g., the error rate for 5xx responses, P99 latency per route). Without labels, you'd have one undifferentiated number.
One middleware wraps every request. It starts a timer, lets the request run, then records the duration and increments the counter with the final status code. In Express, that's res.on('finish'); in FastAPI, it's the @app.middleware("http") block with the .time() context manager.
A /metrics endpoint exposes the registry in Prometheus' text format, which Prometheus scrapes every 15 seconds.
The key point: both services emit the same metric names and labels, so Prometheus sees one uniform schema regardless of language, which is exactly why a single PromQL query in Step 5 works across every service.
Step 3: Scrape and Visualize the Metrics
The goal of this step is to get that telemetry into Prometheus and make it visible.
Prometheus scrapes each service every 15 seconds. Static targets handle known services; a Kubernetes service-discovery config with pod-annotation filtering picks up anything new without any config changes. Grafana sits on top for human-readable dashboards.

Figure 2: Prometheus scrape targets.
Each service registers as a target from which Prometheus pulls metrics. Here, both inventory-service:8000/metrics and order-service:3000/metrics show State = UP with single-digit-millisecond scrape times, a confirmation that the Step 2 instrumentation is live and being collected. The kubernetes-pods job shows 0/0 simply because no pods are deployed to a cluster in this local run. A green UP across your services is the green light to move on.

Figure 3: The Grafana dashboard.
Four panels built directly on the Step 2 metrics – request rate (req/s), P99 latency, error rate (%), and pod restarts, broken down per service. This is the baseline: steady request rate, flat latency, near-zero errors. It's the "normal" that the agent learns to deviate from, and the same panels are what spike during the incident in Step 5.
Step 4: Run a Monitoring Loop
In this step, the goal is to move from only watching to watching, reasoning, and acting.
Most observability stops at the dashboard: the system watches, and humans respond. The AI agent service instead runs a loop every 60 seconds that, on each iteration, queries Prometheus for anomalies, passes any findings to Claude for diagnosis, executes the recommended remediation via the Kubernetes API, and autonomously sends a structured report to Slack, with no human in the decision path.
The loop runs in a background daemon thread, so the FastAPI app stays responsive for health checks. Each iteration is self-contained and exception-safe, so a failure in one cycle can't crash the next.
def monitoring_loop():
logger.info("AI Agent monitoring started")
while True:
try:
anomalies = detect_anomalies()
if anomalies:
logger.info(f"Detected {len(anomalies)} anomalies")
for anomaly in anomalies:
metrics_snapshot = query_prometheus(f'{{job="{anomaly["service"]}"}}')
ai_response = analyze_with_ai(anomaly, metrics_snapshot)
action = ai_response.get("action", "alert")
reasoning = ai_response.get("reasoning", "No reasoning provided")
remediation_result = execute_remediation(action, anomaly['service'])
message = build_slack_message(anomaly, ai_response, remediation_result)
send_slack_notification(message)
log_incident({
'service': anomaly['service'],
'anomaly_type': anomaly['type'],
'metrics': anomaly,
'ai_diagnosis': reasoning,
'action': remediation_result,
'status': 'resolved' if action != 'alert' else 'alerted',
'slack_sent': True
})
else:
logger.info("No anomalies detected")
time.sleep(60)
except Exception as e:
logger.error(f"Monitoring loop error: {e}")
time.sleep(60)
Step 5: Detect Anomalies with PromQL
The goal here is to precisely decide what counts as a problem.
Detection rests on three PromQL queries, each targeting a failure mode that matters in e-commerce:
# 1. Five-minute error rate across all 4xx/5xx responses
rate(http_requests_total{status=~"4..|5.."}[5m])
# 2. P99 latency
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))
# 3. Pod restarts in the last 10 minutes (crash-loop signal)
increase(kube_pod_container_status_restarts_total[10m])The 1% error-rate threshold isn't arbitrary: below it, errors are usually bots, crawlers, and malformed requests; above it, real customers are failing at checkout. Above one second of P99 latency, the hit to conversion is well established.
More than one restart in ten minutes is a crash loop, not a blip. Each confirmed anomaly becomes a structured record – service, type, observed value, and threshold – that serves as Claude's starting point.

Figure 4: The error-rate query lives in Prometheus.
This is the first PromQL query above: rate(http_requests_total{status=~"4..|5.."}[5m]) run in the Prometheus expression browser (table view). It returns the per-series rate of 4xx/5xx responses over the last five minutes.
Most series sit at 0 (healthy traffic), but two stand out: a flood of 404s to /inventory/does-not-exist the inventory service (~2.82/s) and 500s on order-service's /orders/:id (~2.82/s). Both are hundreds of times above the 0.01 (1%) threshold, exactly the signal the agent detects and hands to Claude for diagnosis.
Step 6: Turn Claude into a Diagnosis Engine
The goal of this step is to obtain a decision that the automation can act on, not a paragraph it must interpret.
When an anomaly fires, the agent doesn't notify; it first asks Claude to think.
Claude receives the anomaly details and a full metrics snapshot and is prompted to respond as a senior DevOps engineer, returning a strict JSON contract:
{
"action": "restart | scale | alert",
"severity": "low | medium | high | critical",
"summary": "...",
"root_cause": "...",
"impact": "...",
"reasoning": "...",
"remediation_steps": ["..."]
}
The action field maps directly to a code path.
A restart means the service is in a bad state (memory leak, deadlock, or exhausted connection pool) and needs cycling.
'scale' means it's healthy but overwhelmed, and 'alert' means the problem is outside the system's reach and needs a human.
Claude also assigns a severity and writes out its full reasoning, which is what makes the eventual Slack message worth reading.
def analyze_with_ai(anomaly, metrics_snapshot):
if not anthropic_client:
return {
"action": "alert",
"severity": "medium",
"summary": "AI not configured",
"root_cause": "N/A",
"impact": "N/A",
"reasoning": "AI not configured",
"remediation_steps": ["Check AI agent configuration"]
}
prompt = f"""You are a senior DevOps AI agent monitoring a Kubernetes microservices platform called PulseShield AI.
An anomaly has been detected. Analyze it thoroughly and respond with a detailed incident report.
Anomaly Details:
{json.dumps(anomaly, indent=2)}
Metrics Snapshot:
{json.dumps(metrics_snapshot, indent=2)}
Provide your response as JSON only with this exact structure:
{{
"action": "restart|scale|alert",
"severity": "low|medium|high|critical",
"summary": "one sentence summary of the issue",
"root_cause": "likely root cause of the anomaly",
"impact": "what is the impact on users and the system",
"reasoning": "detailed explanation of your analysis",
"remediation_steps": ["step 1", "step 2", "step 3"],
"scale_replicas": 3
}}"""
try:
message = anthropic_client.messages.create(
model=CLAUDE_MODEL,
max_tokens=2048,
messages=[{"role": "user", "content": prompt}]
)
response_text = message.content[0].text
start = response_text.find("{")
end = response_text.rfind("}") + 1
if start == -1 or end == 0:
raise ValueError(f"No JSON in response: {response_text[:200]}")
return json.loads(response_text[start:end], strict=False)
except Exception as e:
logger.error(f"AI analysis failed: {e}")
return {
"action": "alert",
"severity": "high",
"summary": f"Anomaly detected on {anomaly.get('service', 'unknown')}",
"root_cause": f"Metric value {anomaly.get('value')} exceeded threshold {anomaly.get('threshold')}",
"impact": "Service degradation detected. Users may experience errors or slow responses.",
"reasoning": f"Automated detection triggered. AI analysis unavailable: {str(e)}",
"remediation_steps": [
f"Check logs: kubectl logs -l app={anomaly.get('service')} -n pulseshield",
f"Check pods: kubectl get pods -n pulseshield",
f"Restart: kubectl rollout restart deployment/{anomaly.get('service')} -n pulseshield",
"Monitor metrics in Grafana dashboard",
"Escalate to on-call engineer if issue persists"
]
}
What Claude is asked to return. The prompt forces the answer to be a single JSON object with a fixed set of fields – JSON only, no prose around it. Each field has a job:
action - the one thing to do: restart, scale, or alert. This is the only field the automation acts on.
severity – low, medium, high, or critical; drives the indicator in the Slack message.
summary – a one-line description of what's wrong.
root_cause - Claude's best explanation of why it's happening.
impact – who and what are affected (users, checkout, downstream services).
reasoning – the full explanation behind the diagnosis; this is what makes the Slack alert worth reading.
remediation_steps – an ordered list for a human, in case the automated action isn't enough.
scale_replicas - a suggested replica count when the action is scale.
Because the shape is fixed, the agent reads action and maps it straight to a code path, while the human-readable fields drop into the incident report. Demanding JSON in this exact structure is what makes the model's answer safe to automate against; there's no free text to misparse.
Step 7: Let the Diagnosis Drive Kubernetes
The goal in this step is to turn a decision into a safe, real action.
Claude's action field directly drives the Kubernetes API. Two actions are automated; the alert is intentionally left for humans.
Restart: This uses a deployment annotation patch (kubectl.kubernetes.io/restartedAt), not a pod delete, so Kubernetes performs a rolling restart, and the service stays available throughout with zero dropped requests.
Scale: This is additive – always current replicas + 2, never an absolute number. If an operator has already scaled a service from 2 to 8 during a traffic event, the agent adds capacity rather than overwriting that human decision.
def execute_remediation(action, service, namespace="pulseshield"):
try:
if action == "restart":
k8s_apps.patch_namespaced_deployment(
name=service,
namespace=namespace,
body={"spec": {"template": {"metadata": {"annotations": {"kubectl.kubernetes.io/restartedAt": datetime.now().isoformat()}}}}}
)
logger.info(f"Restarted deployment: {service}")
return "restarted"
elif action == "scale":
deployment = k8s_apps.read_namespaced_deployment(service, namespace)
current_replicas = deployment.spec.replicas
new_replicas = current_replicas + 2
k8s_apps.patch_namespaced_deployment_scale(
name=service,
namespace=namespace,
body={"spec": {"replicas": new_replicas}}
)
logger.info(f"Scaled {service} from {current_replicas} to {new_replicas}")
return f"scaled to {new_replicas}"
return "alert_only"
except Exception as e:
logger.error(f"Remediation failed: {e}")
return f"failed: {e}"
The agent's cluster access is scoped through a dedicated ServiceAccount and ClusterRole that grants only get/list/patch/update on deployments and deployments/scale, plus read on pods. Nothing else, least privilege by design.
apiVersion: v1
kind: ServiceAccount
metadata:
name: ai-agent-sa
namespace: {{ .Values.global.namespace }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: ai-agent-role
rules:
- apiGroups: ["apps"]
resources: ["deployments", "deployments/scale"]
verbs: ["get", "list", "patch", "update"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: ai-agent-binding
subjects:
- kind: ServiceAccount
name: ai-agent-sa
namespace: {{ .Values.global.namespace }}
roleRef:
kind: ClusterRole
name: ai-agent-role
apiGroup: rbac.authorization.k8s.io
NOTE: The restart/scale actions execute against a Kubernetes cluster. Running the stack locally without a cluster, the agent still detects, diagnoses, reports, and logs – it simply records 'alert_only' for the action.
What the RBAC grants. Three objects work together:
ServiceAccount (ai-agent-sa): The identity the agent's pod runs as.
ClusterRole (ai-agent-role): The permissions, and the part that matters: it allows only get, list, patch, and update on deployments and deployments/scale (exactly enough to trigger a rolling restart and to scale), plus get/list/delete on pods. Nothing else.
ClusterRoleBinding (ai-agent-binding): Ties the role to the service account.
Step 8: Deliver the Incident Report to Slack
The goal here is to make Slack the place where complete briefings land.
After every cycle, the agent posts a structured report built from every field of Claude's response: a severity indicator, the metric value next to its threshold, and the AI analysis – summary, root cause, user impact, reasoning, and ordered remediation steps.

Figure 5: A screenshot of the incident report Claude posts to Slack.
Compare what an engineer reads here at 2:47 AM versus a traditional alert. The old alert says, "Something is wrong with order service."
This says, 'Here is what happened, here is why, here is what the system already did, and here is what to do if that wasn't enough.' That's the difference between being woken to solve a problem and being woken to review a solution.
Step 9: Persist Every Incident
Here, the goal is to give the system a memory.
Before the Slack message goes out, every incident is written to a PostgreSQL incidents table:
CREATE TABLE IF NOT EXISTS incidents (
id SERIAL PRIMARY KEY,
timestamp TIMESTAMP DEFAULT NOW(),
service VARCHAR(255),
anomaly_type VARCHAR(255),
metrics_snapshot TEXT, -- full Prometheus response: reproducible
ai_diagnosis TEXT, -- Claude's reasoning, not just the action
remediation_action VARCHAR(255),
status VARCHAR(50),
slack_sent BOOLEAN DEFAULT FALSE
);
What the schema stores. Each incident becomes one row, written before the Slack message goes out:
id / timestamp - a unique ID and when the incident was recorded.
service / anomaly_type - which service tripped and what kind of anomaly (high error rate, high latency, crash loop).
metrics_snapshot - the full Prometheus response at the moment of detection, so the incident is fully reproducible later.
ai_diagnosis - Claude's reasoning: why it decided what it did, not just the action.
remediation_action - what the agent actually did (restarted, scaled, or alert_only).
status - resolved if an action was taken or alerted if it was handed to a human.
slack_sent - whether the notification was delivered.

Figure 6: The incidents table
The audit trail of recent anomalies, each with action, status, and timestamp. (alert_only here because this run had no cluster; a live one would also show restarted/scaled.)

Figure 7: The stored AI diagnosis
This is the ai_diagnosis column for the latest incident, holding Claude's full reasoning (traffic breakdown + error-rate math). The table records why, not just the action.
Over time, this table becomes a dataset: you can measure how often automated restarts actually resolve incidents versus when a human is needed, see which services fail most often, and audit and tune the agent's decisions. The system doesn't learn on its own, but the table is what lets you make it better.
Possible tradeoff to consider for this setup
1. Why not just use AlertManager?
AlertManager is excellent, and this isn't a criticism of it. But its rules are static: IF error_rate > 0.01 FOR 5m THEN page fires identically whether the cause is a bad deploy, a saturated database, or a DDoS spike, yet the right response to each is completely different. Claude reads context; a static rule can't. The two are complementary: AlertManager for routing and dedup at scale, and the agent for intelligent triage when interpretation matters.
2. Why Slack, not PagerDuty?
Slack is where the team already lives, and it carries the rich, context-heavy briefing. In production, you'd run both – PagerDuty to wake people up for critical incidents and Slack to tell them what happened before they're fully awake.
What Does This Change About On-Call?
The most important shift isn't speed of remediation; it's the nature of the work that remains.
Traditionally, the on-call engineer is the intelligence layer that receives a signal, applies judgment, decides, and executes cognitively demanding work, done worst at 2:47 AM on broken sleep.
This agent, however, moves that intelligence into the system for incidents with clear remediation patterns, such as connection pool exhaustion, traffic spikes, and crash loops.
For those, the engineer becomes an auditor: review a completed action with full context, confirm it was right, and go back to sleep. For everything else, they arrive at a Slack message that has already done the diagnostic work. They still make the call – just faster and far better informed.
The Gap Is Smaller Than You Think
The distance between a modern observability stack and a self-healing platform is not a multi-year project.
The core pattern of detect, diagnose, and act is a few hundred lines of Python and a well-structured Claude prompt on top of Prometheus. The key insight is to treat Claude not as a chatbot but as a structured reasoning engine: precise inputs and precise machine-readable outputs, with nuance preserved in the fields humans read rather than the code paths that execute.
Prometheus collects the signal, Claude interprets it, Kubernetes acts on the interpretation, and Slack keeps humans in the loop with enough context to trust and verify every decision.
For e-commerce, especially, where every minute of degraded service costs real money in abandoned carts, the case isn't about engineering quality of life. It's about systems that are resilient by default, platforms that don't wait to be told something is wrong because they already know and have already acted.


