# Troubleshooting X Search Failures in last30days-skill: Root Causes and Fixes

> Fix X search failures in last30days-skill by identifying root causes like missing dependencies, bad cookies, or generic queries and applying effective solutions.

- Repository: [Matt Van Horn/last30days-skill](https://github.com/mvanhorn/last30days-skill)
- Tags: troubleshooting
- Published: 2026-03-25

---

**X searches in last30days-skill fail most often due to missing Node.js dependencies, invalid authentication cookies, or overly generic queries that exhaust the three-tier retry fallback system implemented in [`bird_x.py`](https://github.com/mvanhorn/last30days-skill/blob/main/bird_x.py).**

The last30days-skill repository relies on a sophisticated Python-to-Node.js bridge to query X’s GraphQL API. When troubleshooting X search failures in last30days-skill, understanding the interaction between the **bird_x.py** wrapper and the vendored Bird module is essential for resolving credential, timeout, and query parsing issues quickly.

## How the X Search Architecture Works

### The bird_x.py Wrapper Layer

According to the mvanhorn/last30days-skill source code, the X search capability is implemented in **[`scripts/lib/bird_x.py`](https://github.com/mvanhorn/last30days-skill/blob/main/scripts/lib/bird_x.py)**. This file serves as a thin Python wrapper around a vendored **Bird** Node.js module located at `scripts/lib/vendor/bird-search/bird-search.mjs`. The wrapper handles subprocess management, credential injection, and robust retry logic while the underlying Node.js component communicates directly with X’s GraphQL API.

The wrapper exposes several key functions:

- **`set_credentials()`** (lines 33‑50) – Injects `AUTH_TOKEN` and `CT0` cookies from a `.env` file into the subprocess environment
- **`is_bird_installed()`** (lines 74‑83) – Verifies that `bird-search.mjs` exists and that Node.js is available on `$PATH`
- **`is_bird_authenticated()`** (lines 85‑100) – Checks for injected credentials or tests browser cookie accessibility via `node … --whoami`
- **`_run_bird_search()`** (lines 151‑165) – Executes the Node process with timeout enforcement and JSON output capture
- **`search_x()`** (lines 48‑84) – Orchestrates the three-tier retry strategy and returns results or error dictionaries

### Authentication and Environment Configuration

The **`_subprocess_env()`** helper merges optional credentials into the environment before spawning the Node process. You can inject credentials manually via `set_credentials()` or rely on the automatic loading in [`scripts/lib/env.py`](https://github.com/mvanhorn/last30days-skill/blob/main/scripts/lib/env.py), which reads from your `.env` file during startup.

### Core Subject Extraction and Retry Strategy

When `search_x()` receives a verbose natural language query like *"best AI tools for coding"*, it delegates to **`_extract_core_subject()`** (lines 63‑71), which calls `query.extract_core_subject()` to generate a concise literal keyword list that X can match.

If the initial literal search returns zero items, `search_x()` automatically executes three progressive fallbacks:

1. **OR-group expansion** – Searches compound terms as `"Claude plugin" OR "Code plugin"`
2. **Shortened two-word query** – Reduces to the most significant two-word phrase
3. **Strongest token fallback** – Searches only the strongest non-stopword single token

## Common X Search Failure Modes and Solutions

### Empty Result Sets After Progressive Retries

**Symptom:** The response contains `"items": []` even after all three retry attempts.

**Root cause:** The core subject extracted from your query is too generic (e.g., "trendiest") or X’s index contains no matching tweets within the requested date window.

**Fix:** Rephrase your query with specific technical terms or widen the `from_date` and `to_date` parameters. Check the verbose logs to see which fallback branch was exhausted.

### Timeout and Process Management Errors

**Symptom:** Response contains `{"error":"Search timed out …"}`.

**Root cause:** Network slowness, X API throttling, or a deadlocked Node process.

**Technical details:** The `_run_bird_search()` function enforces strict timeouts based on search depth: **30 seconds** for "quick", **45 seconds** for "default", and **60 seconds** for "deep". When timeouts trigger, the function kills the entire process group to prevent zombie processes.

### Installation and Environment Errors

**Symptom:** Response contains `{"error":"Bird search failed"}` or `is_bird_installed()` returns `False`.

**Root cause:** The `node` binary is missing from `$PATH`, or `scripts/lib/vendor/bird-search/bird-search.mjs` does not exist.

**Fix:** Verify Node.js version 22+ is installed and accessible. Confirm the vendored Bird module exists at the expected path:

```bash
ls scripts/lib/vendor/bird-search/bird-search.mjs
node --version

```

### Invalid JSON and Authentication Failures

**Symptom:** Response contains `{"error":"Invalid JSON response"}` or authentication warnings.

**Root cause:** The Bird module crashed and printed a stack trace instead of valid JSON, or `AUTH_TOKEN`/`CT0` cookies are missing/invalid.

**Technical details:** `_run_bird_search()` catches `json.JSONDecodeError` when the subprocess outputs non-JSON data. For authentication issues, `is_bird_authenticated()` returns `None` when neither environment credentials nor browser cookies are available, causing the research engine in [`scripts/last30days.py`](https://github.com/mvanhorn/last30days-skill/blob/main/scripts/last30days.py) (lines 346‑356) to skip X searches entirely.

## Practical Debugging Techniques

### Enabling Verbose Logging

All internal events are written to **stderr** via the internal `_log()` function. Capture diagnostic output by redirecting stderr when invoking the skill:

```bash
python3 -m scripts.last30days "Claude plugins" 2>bird_debug.log

```

Typical log output showing the retry progression:

```

[Bird] Searching: Claude plugins since:2026-02-01
[Bird] 0 results for 'Claude plugins', retrying with OR groups: "Claude plugin" OR "Code plugin"
[Bird] 0 results for 'Claude plugins', retrying with 'Claude plugins'
[Bird] 0 results for 'Claude plugins', retrying with strongest token 'Claude'

```

### Checking Installation and Authentication Status

Run this diagnostic script to verify your environment:

```python
from lib import bird_x

print("Installed?", bird_x.is_bird_installed())
print("Authenticated?", bird_x.is_bird_authenticated())

```

Expected healthy output:

```

Installed? True
Authenticated? env AUTH_TOKEN

```

If `Installed?` is `False`, reinstall dependencies. If `Authenticated?` is `None`, check your `.env` file for `AUTH_TOKEN` and `CT0` values or ensure the Bird module can access browser cookies.

### Performing a Manual Search Test

Isolate X search issues by calling the wrapper directly:

```python
from lib import bird_x

# Optional: Explicitly set credentials

bird_x.set_credentials(
    auth_token="YOUR_AUTH_TOKEN",
    ct0="YOUR_CT0_COOKIE"
)

# Execute search for last 30 days

result = bird_x.search_x(
    topic="Rust web frameworks",
    from_date="2026-01-01",
    to_date="2026-01-30",
    depth="default"
)

if result.get("error"):
    print(f"Error type: {result['error']}")
else:
    print(f"Retrieved {len(result.get('items', []))} tweets")

```

## Summary

- **X search failures** in last30days-skill originate in [`scripts/lib/bird_x.py`](https://github.com/mvanhorn/last30days-skill/blob/main/scripts/lib/bird_x.py), which wraps a Node.js subprocess communicating with X’s GraphQL API.
- **Authentication** requires valid `AUTH_TOKEN` and `CT0` cookies injected via `set_credentials()` or accessible browser cookies.
- **Timeouts** are enforced at 30s/45s/60s depending on search depth; persistent hangs indicate network issues or X throttling.
- **Empty results** trigger three automatic retries (OR-groups, shortened phrases, strongest token), but overly generic queries may still return nothing.
- **Installation errors** occur when Node.js is missing or `bird-search.mjs` is not present in `scripts/lib/vendor/bird-search/`.

## Frequently Asked Questions

### Why does my X search return zero results even after retries?

If the progressive fallback strategy in `search_x()` (lines 48‑84) exhausts all three attempts, the query is likely too generic or the date window is too narrow. The code first tries the literal query, then OR-grouped compound terms, then a shortened two-word version, and finally the strongest single token. If X’s index contains no matches for any variant, the skill returns an empty `"items"` list. Try rephrasing with specific technical keywords or widening the `from_date` and `to_date` parameters.

### How do I verify that Bird is properly installed and accessible?

Call `bird_x.is_bird_installed()` to check for the presence of `scripts/lib/vendor/bird-search/bird-search.mjs` and a Node.js binary on `$PATH`. This function (lines 74‑83) returns `True` only if both the vendored module and the Node runtime are detected. If it returns `False`, verify your Node installation is version 22 or higher and that the vendor directory exists.

### What causes "Search timed out" errors and how long should I wait?

The `_run_bird_search()` function (lines 151‑165) enforces timeouts of **30 seconds** for "quick" searches, **45 seconds** for "default", and **60 seconds** for "deep" searches. These timeouts protect against network slowness or X API throttling. If you encounter this error consistently, the Node subprocess may be hanging on authentication or rate limiting. Check that your `AUTH_TOKEN` is valid and consider reducing search depth to "quick".

### Can I use browser cookies instead of manually setting AUTH_TOKEN and CT0?

Yes. The `is_bird_authenticated()` function (lines 85‑100) first checks for explicitly injected credentials, then attempts to run `node bird-search.mjs --whoami` to verify if the vendored Bird module can read cookies from your default browser. If this succeeds, the skill will use those cookies automatically. If both methods fail, the caller in [`scripts/last30days.py`](https://github.com/mvanhorn/last30days-skill/blob/main/scripts/last30days.py) will skip X searches or emit a warning, depending on the configuration.