# Proposal 114: MCP Interface Issues from 10-Plan Agent Stress Test

**Date:** 2026-03-11
**Status:** Proposal (I1, I2, I4 implemented; remainder open)
**Source:** Feedback from Claude (Opus 4.6) after four stress-testing sessions (17 plans total) operated through Claude Code CLI.
**Scope:** MCP interface design, observability, and lifecycle management. Does not cover plan content quality or pipeline output improvements.

---

## 1. Context

Four stress-testing sessions were conducted through Claude Code CLI using the PlanExe MCP interface:

- **Session 1** (10 plans): Surfaced core MCP interface friction points — state ambiguity, missing diagnostics, lifecycle gaps.
- **Session 2** (2 plans, with stop/resume cycling): Validated the `stopped` state implementation and identified remaining gaps.
- **Session 3** (1 plan on remote server): First use of `planexeremote` alongside `planexelocal`. Exposed local-vs-remote behavioral differences and prompted a fundamental correction on SSE's role for agents.
- **Session 4** (4 plans, including MCP notification test): First test of `monitor=true` MCP notifications. Validated error dict in a real content-safety failure. Operated the most complex plan lifecycle (stop → resume → fail → resume → complete). Discovered that Claude Code silently drops MCP progress notifications.

The testing surfaced concrete MCP interface friction points that affect agent operators — issues with state management, observability, lifecycle hygiene, and workflow ergonomics. This proposal catalogues the MCP-specific issues and maps them against existing proposals.

---

## 2. Issues

### I1 — `failed` state conflates user-stop and actual failure

**Status:** Implemented (Option A — dedicated `PlanState.stopped` DB state). Supersedes PR #244's Option B (`stop_reason` field).

**Problem:** When `plan_stop` is called, the plan transitions to `failed`. When a worker crashes, it also transitions to `failed`. The agent operator cannot distinguish between:
- User-initiated stop (nothing went wrong)
- Actual failure (network drop, model error, worker crash)

This matters because the correct response differs: user stop suggests `plan_resume`; actual failure may need `plan_retry` or investigation.

**Overlap:** Proposal 87 (plan_resume) acknowledged this in §4 — now updated to reflect the implementation.

**Options:**

| Option | Description | Breaking change? |
|--------|-------------|-----------------|
| A. New `stopped` state | `plan_stop` → `stopped`, worker crash → `failed` | Yes (new enum value in API responses) |
| B. `stop_reason` field | Keep `failed` but add `stop_reason`: `"user_requested"`, `"worker_crash"`, `"timeout"`, `"model_error"`, `null` | No |

Option B is less disruptive and can be added to `plan_status` without changing state enum values.

**Implemented approach:** Option A — a dedicated `PlanState.stopped` enum value (value 5). `plan_stop` now transitions plans to the `stopped` state instead of `failed`. The `stop_reason` field introduced in PR #244 (Option B) has been removed — the state itself communicates intent. `plan_retry` and `plan_resume` accept both `failed` and `stopped` states. DB migration adds `'stopped'` to the PostgreSQL enum type (both `taskstate` for pre-rename databases and `planstate` for fresh databases — the Python class was renamed from `TaskState` to `PlanState` in proposal 74 but the PostgreSQL type name was not changed). The worker's post-pipeline finalization also transitions to `stopped` (not `failed`) when `stop_requested` is true.

**Affected files:** `database_api/model_planitem.py`, `mcp_cloud/db_setup.py`, `worker_plan_database/app.py`, `mcp_cloud/db_queries.py`, `mcp_cloud/handlers.py`, `mcp_cloud/sse.py`, `mcp_cloud/tool_models.py`, `mcp_cloud/schemas.py`, `frontend_multi_user/src/app.py`, `frontend_multi_user/src/planexe_modelviews.py`, `frontend_multi_user/templates/plan_iframe.html`, `frontend_multi_user/templates/run_via_database.html`, `frontend_multi_user/templates/index.html`, `frontend_multi_user/templates/account.html`, `mcp_cloud/tests/test_plan_status_tool.py`, `docs/mcp/planexe_mcp_interface.md`, `docs/mcp/autonomous_agent_guide.md`, `docs/proposals/done/87-plan-resume-mcp-tool.md`, `docs/proposals/111-promising-directions.md`.

---

### I2 — No failure diagnostics in `plan_status`

**Status:** Implemented. Four new nullable columns on `PlanItem` (`failure_reason`, `failed_step`, `error_message`, `recoverable`) populated by the worker on failure and surfaced in `plan_status` responses inside a consolidated `error` dict. Diagnostics are reset on retry/resume.

**Priority: High — identified as the single biggest observability gap.**

**Problem:** When a plan fails, `plan_status` returns `state: "failed"` with no `failure_reason`, `error_message`, or `failed_step` field. The agent can only tell the user "it failed" without explaining why.

During the stress test, Plan 1 (20f1cfac) stalled at 5.5% with zero diagnostic information. The operator had no way to determine if it was a content refusal, a model error, or a worker crash.

**Overlap:** Proposal 113 (LLM Error Traceability) preserves errors in usage metrics logs but does not surface them in `plan_status` responses to the MCP consumer.

**Implemented response shape (on failure):**

```json
{
  "state": "failed",
  "error": {
    "failure_reason": "generation_error",
    "failed_step": "expert_criticism",
    "message": "LLM provider returned 503",
    "recoverable": true
  }
}
```

The `recoverable` boolean lets the agent immediately suggest `plan_resume` (transient/recoverable) vs `plan_retry` (fundamental/non-recoverable) without guessing.

**`failure_reason` values:**
- `generation_error` — pipeline step failed (recoverable)
- `worker_error` — unhandled exception in worker (recoverable)
- `inactivity_timeout` — stop flag detected without user request (recoverable)
- `internal_error` — pipeline completed but no report produced (not recoverable)
- `version_mismatch` — resume attempted with incompatible pipeline version (not recoverable)

**Implementation:** Four dedicated nullable columns on `PlanItem`: `failure_reason`, `failed_step`, `error_message`, `recoverable`. The column was originally named `last_error` and renamed to `error_message` to match the consolidated `error.message` field in the MCP response. Migration functions in all three services (mcp_cloud, worker_plan_database, frontend_multi_user) check column existence via `sqlalchemy.inspect` before attempting the rename — this avoids noisy PostgreSQL ERROR logs on restarts where the rename already succeeded. The worker populates them via `_update_failure_diagnostics()` at three failure paths: normal pipeline failure, unhandled exception, and version mismatch on resume. The `plan_status` handler consolidates these into a single `error` dict (with keys `failure_reason`, `failed_step`, `message`, `recoverable`) only when `state == "failed"` — non-failed plans omit the `error` field entirely. Diagnostics are cleared on `plan_retry` and `plan_resume`. The frontend displays diagnostics in the failure trace panel and the admin bulk-fail form uses the renamed field.

**Affected files:** `database_api/model_planitem.py`, `mcp_cloud/db_setup.py`, `mcp_cloud/db_queries.py`, `mcp_cloud/handlers.py`, `mcp_cloud/tool_models.py`, `mcp_cloud/schemas.py`, `mcp_cloud/tests/test_plan_status_tool.py`, `mcp_cloud/README.md`, `worker_plan_database/app.py`, `frontend_multi_user/src/app.py`, `frontend_multi_user/src/planexe_modelviews.py`, `frontend_multi_user/templates/admin/bulk_change_to_failed.html`, `frontend_multi_user/templates/plan_iframe.html`, `docs/mcp/planexe_mcp_interface.md`, `docs/mcp/mcp_details.md`, `docs/mcp/autonomous_agent_guide.md`.

---

### I3 — No plan deletion or cleanup

**Problem:** Stopped and failed plans persist in `plan_list` indefinitely. After 10 plans, the list becomes noisy. There is no way to clean up test runs, abandoned plans, or experiments.

**Proposed solutions (pick one or both):**

| Tool | Behavior |
|------|----------|
| `plan_delete` | Permanently remove a plan from the user's list. Only allowed for terminal states (`completed`, `failed`, `stopped`). |
| `plan_archive` | Soft-delete: plan is hidden from `plan_list` but remains in DB for auditing. |

`plan_archive` is safer for a billing system where plan records may need to be retained.

**Affected files:** `mcp_cloud/db_queries.py`, `mcp_cloud/handlers.py`, `mcp_cloud/schemas.py`, `mcp_cloud/tool_models.py`.

---

### I4 — No idempotency guard on `plan_create`

**Status:** Implemented (PR #242).

**Problem:** If the user double-clicks or the agent accidentally submits twice, two identical plans are created. Each `plan_create` call generates a new `plan_id` with no server-side deduplication.

**Original proposal:** Add an optional `request_id` (client-generated UUID) parameter to `plan_create`. The server checks if a plan with the same `request_id` already exists for this user. This is a standard idempotency pattern (Stripe, AWS, etc.).

**Why `request_id` was not adopted:** The primary MCP consumers are LLM agents. LLMs cannot generate UUIDs natively — they would need an extra tool call just to produce the idempotency key, adding friction to the exact workflow it's meant to protect. A mechanism that requires client cooperation is a poor fit when the clients are language models.

**Implemented approach:** Automatic server-side dedup. Before inserting a new plan, `_create_plan_sync` queries for an existing `pending`/`processing` plan matching `(user_id, prompt, model_profile)` created within a configurable time window (default 10 minutes, env `PLANEXE_DEDUP_WINDOW_SECONDS`). If found, the existing plan is returned with `deduplicated: true` instead of creating a new one. No schema migration needed — uses existing columns. Set `PLANEXE_DEDUP_WINDOW_SECONDS=0` to disable.

**Known limitation:** There is a TOCTOU race — if two identical requests arrive concurrently and both pass the dedup check before either commits, a duplicate plan is created. This is accepted; the cost is wasted tokens for one extra plan, which is not worth the complexity of a database-level lock or migration to fix.

**Affected files:** `mcp_cloud/db_queries.py` (`_find_recent_duplicate_plan`, `_create_plan_sync`), `mcp_cloud/tool_models.py` (`deduplicated` field on `PlanCreateOutput`), `mcp_cloud/schemas.py` (description and `idempotentHint`), `mcp_cloud/app.py` (re-export).

---

### I5 — SSE is the wrong mechanism for MCP agents

**Status:** Implemented and reverted. A `monitor` boolean parameter was added to `plan_create`, `plan_retry`, and `plan_resume` using the MCP SDK's `ctx.report_progress()` and `ctx.info()` APIs. The server sent `notifications/progress` and `notifications/message` correctly, but v4 real-world testing revealed that Claude Code — PlanExe's primary MCP consumer — silently drops both notification types. Claude Code only supports `list_changed` notifications (for refreshing tool/resource definitions). Since no current MCP client can consume the notifications, the implementation was reverted as unused code.

**v4 real-world testing (March 2026, session 4):** A `monitor=true` parameter was implemented and tested. Claude Code tested `monitor=true` on `plan_resume`. The resume succeeded and the plan completed, but zero notifications were received. Investigation showed that Claude Code only supports `list_changed` MCP notifications (for refreshing tool/resource definitions). It does not handle `notifications/progress` or `notifications/message` — they are silently dropped. The server was sending notifications correctly; the gap is on the client side. The implementation was reverted as unused code — no MCP client can consume it today. See session 4 observations in section 7.

**Corrected priority ranking (from v4 agent feedback):**

1. Polling `plan_status` — reliable, works everywhere, recommended default
2. `plan_wait` blocking tool (see I8) — would eliminate polling for agents without notification support
3. MCP notifications — correct long-term solution, blocked by client support (revisit when clients add `notifications/progress` handling)
4. SSE — only useful for non-agent consumers (browser UIs, dashboards)
5. HTTP webhooks — only useful for server-to-server

**Updated in v3:** The original framing ("SSE events lack structured data") missed the deeper problem. SSE is designed for real-time UI clients, not turn-based agents.

**Problem:** The agent's SSE monitoring pattern across all 13 plans was:
```
curl -N -s <sse_url> 2>&1 | tail -5   # run in background
```
The agent never reads SSE event content. It waits for `curl` to exit (connection close) and treats that as a "plan finished" signal — abusing a data stream as a binary trigger. When the remote SSE stream dropped at ~48% progress, the agent assumed the plan was done, checked `plan_status`, and discovered it was still running. The failure wasn't in SSE — it was in the pattern.

Even if SSE events contained rich structured progress data, a turn-based MCP agent wouldn't see them until the stream closes, because `curl` runs in the background and output is only read on task completion. MCP agents cannot "watch" a stream in real time.

**What the agent previously asked for (wrong):**
- "Richer SSE events with progress data" — useless, since event content is never read
- "Webhooks" — HTTP callbacks, but CLI agents have no endpoint to register
- "SSE heartbeat pings" — would only help if the pattern itself were sound

**What would actually help MCP agents (corrected in v3, re-corrected in v4):**

The v3 priority ranking assumed MCP clients would surface progress notifications. Real-world testing in v4 showed this is not the case (Claude Code silently drops them). See corrected priority ranking above.

1. **Polling `plan_status` (works everywhere):** The primary method. Reliable across all clients and transports.

2. **`plan_wait` blocking tool:** See I8. Would eliminate polling for agents without notification support.

3. **MCP notifications:** Correct long-term solution, but blocked by client support. Revisit when MCP clients add `notifications/progress` and `notifications/message` handling.

4. **SSE (for non-MCP real-time clients only):** Keep SSE for browser UIs, streaming dashboards, and CLI scripts that can consume events in real time. Do not recommend to MCP agents.

**Overlap:** Proposal 70 §5.1 (SSE progress streaming) is implemented but serves the wrong consumer type for agent use cases.

**Affected files:** `mcp_cloud/sse.py`, `mcp_cloud/schemas.py` (tool description update to stop recommending SSE to agents), `mcp_cloud/handlers.py` (MCP notification support).

---

### I6 — Download URL expiry (15 minutes) is too short

**Problem:** When a user takes time reviewing results before downloading, the signed download URL expires silently. Calling `plan_file_info` again works, but the 15-minute window creates unnecessary friction.

**Proposed fix:** Extend default TTL from 15 to 30–60 minutes. Make it configurable via `PLANEXE_DOWNLOAD_TOKEN_TTL_SECONDS` environment variable.

**Affected files:** `mcp_cloud/download_tokens.py` (change default TTL constant, add env var override).

---

### I7 — No stalled-plan detection *(partially implemented)*

**Problem:** Plan 1 (20f1cfac) stayed at 5.5% for multiple status checks with no indication of whether it was still working, stuck, or had silently failed. There is no `stalled_since` timestamp or heartbeat indicator in `plan_status`.

**Overlap:** Proposal 87 §8 proposes `current_task` and `last_llm_call_at` fields — these would partially address this, but there is no explicit stall detection.

**Proposed additions to `plan_status`:**

| Field | Description | Status |
|-------|-------------|--------|
| `last_progress_at` | ISO 8601 timestamp of the last progress update. Enables the agent to compute time-since-last-progress and decide whether to wait, resume, or retry. | ✅ Implemented |
| `last_llm_call_at` | ISO 8601 timestamp of the most recent LLM call. A gap > 5 minutes with no progress change is a strong stall signal. | Deferred |

**Affected files:** `worker_plan_database/app.py` (write timestamps on progress), DB model (new timestamp columns), `mcp_cloud/handlers.py` (include in response).

**Implementation note:** `last_progress_at` is written by the worker on every progress update and returned in `plan_status` under `timing.last_progress_at`. It resets to `null` on `plan_retry` and is preserved on `plan_resume`. `last_llm_call_at` is deferred — it requires deeper integration with the LLM executor layer.

---

### I8 — No `plan_wait` tool for agents without shell access

**Problem:** The recommended SSE monitoring pattern (`curl -N <sse_url>`) requires Bash tool access. Agents running in pure MCP client environments (no shell) can only poll `plan_status`. A blocking `plan_wait` tool would let these agents simply wait for completion.

**Proposed tool:**

```
plan_wait(plan_id, timeout_seconds=1200): Blocks until the plan reaches a terminal state
(completed, failed, stopped) or the timeout expires. Returns the final plan_status response.
```

**Implementation:** Server-side long-poll using the existing SSE infrastructure. The handler subscribes to the plan's state changes and returns when terminal or timeout.

**Consideration:** Long-running HTTP requests may conflict with proxy/load-balancer timeouts. Default timeout should be ≤ 20 minutes. May need to be implemented as repeated short polls on the server side with a single response.

**Affected files:** `mcp_cloud/handlers.py`, `mcp_cloud/schemas.py`, `mcp_cloud/tool_models.py`.

---

### I9 — Prompt iteration has no memory

**Problem:** Each `plan_create` is independent. When iterating on a prompt (e.g. refining "The Game" movie remake across 4 variations), there is no way to link plans or diff their prompts. The agent must manually track the relationship between plans.

**Proposed fix:** Add an optional `parent_plan_id` parameter to `plan_create`. When set:
- The new plan is linked to its parent in the DB
- `plan_list` can optionally filter by lineage
- `plan_status` includes `parent_plan_id` in the response

This enables prompt iteration tracking without changing existing behavior for plans created without a parent.

**Affected files:** `mcp_cloud/tool_models.py` (new optional field), `mcp_cloud/db_queries.py` (store parent reference), DB model (new nullable column).

---

### I10 — Silent partial failures in completed plans

**Problem:** A plan can reach `completed` with sections that are empty or stub-quality (e.g. 6/8 experts returning no feedback in the expert criticism step). There is no signal in `plan_status` or `plan_file_info` that the output has quality gaps. The agent has to read individual files to discover this. `completed` means "all 110 steps ran" — not "all sections produced quality output."

This is a trust gap: the agent cannot confidently tell the user "your plan is ready" without caveats, because it has no visibility into per-section quality.

**Proposed addition:** A `quality_summary` in the completed plan status or file info:

```json
{
  "sections_complete": 108,
  "sections_partial": 2,
  "partial_details": [
    {"step": "expert_criticism", "note": "2/8 experts provided feedback"}
  ]
}
```

**Implementation path:** The worker already knows which steps produced output. A post-pipeline validation pass could check key sections for minimum output quality (e.g. non-empty, expected structure present) and write a summary to the DB. `plan_status` surfaces it when `state == "completed"`.

**Affected files:** `worker_plan_database/app.py` (quality validation pass), DB model (quality summary column), `mcp_cloud/handlers.py` (include in completed plan_status response).

---

### I11 — No server identity in responses

**New in v3.** Surfaced during the first session using both `planexelocal` and `planexeremote`.

**Problem:** The agent cannot programmatically determine which server backend it's connected to. When the user ran `/mcp` and reconnected, the active server changed from local to remote without any signal — the only clue was the tool name prefix changing from `mcp__planexelocal__*` to `mcp__planexeremote__*`. If both servers had the same prefix, the switch would be invisible.

This matters because:
- Plans created on local can't be accessed from remote (different plan_id namespaces)
- SSE monitoring patterns that work on local may fail on remote
- Speed expectations differ (local ~13 min, remote ~28 min for baseline plans)

**Proposed fix:** `plan_status` could include a `server` or `endpoint` field so the agent always knows which backend it's talking to. Alternatively, `plan_create` could return a `server_info` object with capabilities:

```json
{
  "server_info": {
    "server_id": "planexe-remote-prod",
    "sse_reliable": false,
    "files_visible_during_processing": false,
    "expected_speed": "slow"
  }
}
```

**Affected files:** `mcp_cloud/handlers.py`, `mcp_cloud/tool_models.py`.

---

### I12 — No files visibility during remote processing

**New in v3.** Surfaced during the first remote plan.

**Problem:** `plan_status` on the remote server returns `files_count: 0` while processing. On local, files appear incrementally. The incremental file list on local was useful for:
- Confirming the plan is actually producing output (not just incrementing step count)
- Seeing which pipeline sections have completed
- Early reading of intermediate files if curious

Losing this on remote makes the plan feel more opaque. The inconsistency also means agent workflows that depend on file visibility during processing will break silently when switching servers.

**Proposed fix:** Align remote behavior with local — populate the files list incrementally during processing. If this isn't feasible due to remote storage architecture, document the difference in the tool description.

**Affected files:** `worker_plan_database/app.py` (remote file list population), `mcp_cloud/schemas.py` (document behavioral difference if not fixed).

---

### I13 — SSE reliability differs between local and remote

**New in v3.** The SSE stream on local was 100% reliable across 12 plans and multiple stop/resume cycles. On remote, the stream dropped at ~48% on the first plan. Possible causes: proxy timeout, load balancer idle timeout, or different SSE implementation.

While I5 argues SSE is the wrong tool for agents regardless of reliability, this inconsistency also affects non-agent consumers (browser UIs, dashboards) that would rely on SSE for real-time progress. If SSE is kept for these consumers, it needs to work reliably on remote too.

**Proposed fix:** Investigate and fix the remote SSE connection stability. Common causes: reverse proxy `proxy_read_timeout` too low, load balancer idle timeout, missing SSE keepalive pings.

**Affected files:** Infrastructure/deployment configuration (not application code).

---

## 3. Cross-Reference with Existing Proposals

| Issue | Existing Proposal | Gap |
|-------|-------------------|-----|
| I1 (stopped vs failed) | 87 §4 (deferred) | **Implemented** (Option A — dedicated `PlanState.stopped`) |
| I2 (failure diagnostics) | 113 (logs only) | **Implemented** — surfaced to MCP consumer |
| I3 (plan_delete) | None | New |
| I4 (idempotency) | None | **Implemented** (PR #242) |
| I5 (SSE wrong for agents) | 70 §5.1 (basic SSE done) | SSE serves wrong consumer type; MCP notifications needed |
| I6 (download TTL) | 70 §4 (tokens done) | TTL too short, not configurable |
| I7 (stall detection) | 87 §8 (partial) | No explicit stall timestamps |
| I8 (plan_wait) | None | New |
| I9 (prompt iteration) | None | New |
| I10 (silent partial failures) | None | New |
| I11 (server identity) | None | New (v3) |
| I12 (remote files visibility) | None | New (v3) |
| I13 (remote SSE reliability) | 70 §5.1 | SSE drops on remote; local is fine |

---

## 4. Overlap with Proposal 70 (MCP Interface Evaluation and Roadmap)

Several items here refine or extend issues already tracked in Proposal 70's checklist:

| This Proposal | Proposal 70 Item | Relationship |
|---------------|-------------------|-------------|
| I2 (failure diagnostics) | 70 §5.6 (pipeline stage names) | Complementary — 70 focuses on progress UX, I2 focuses on failure UX |
| I5 (SSE wrong for agents) | 70 §5.1 (SSE implemented) | Correction — SSE serves wrong consumer type for agents; MCP notifications needed |
| I6 (download TTL) | 70 (signed tokens done) | Refinement — increase TTL |
| I13 (remote SSE reliability) | 70 §5.1 (SSE implemented) | Bug — SSE drops on remote but works on local |

If accepted, I1–I4 and I7–I13 should be added to Proposal 70's quick-win checklist as new line items.

---

## 5. Suggested Priority

| Priority | Issues | Rationale |
|----------|--------|-----------|
| ~~P1~~ | ~~I2 (failure diagnostics)~~ | **Implemented**. Four DB columns populated by worker, surfaced in `plan_status`, displayed in frontend. |
| ~~P1~~ | ~~I1 (stopped vs failed)~~ | **Implemented** (Option A). Dedicated `PlanState.stopped` enum value — `plan_stop` transitions to `stopped`, not `failed`. |
| P1 | I5 (SSE wrong for agents) | MCP notifications should replace SSE as the recommended agent completion mechanism. Highest-impact agent UX improvement. |
| P2 | I7 (stall detection) | Prevents agents from waiting indefinitely on stuck plans. |
| P2 | I6 (download TTL) | Low effort, reduces friction. |
| P2 | I3 (plan_delete) | Hygiene for multi-plan sessions. |
| ~~P3~~ | ~~I4 (idempotency)~~ | **Implemented** (PR #242). Server-side auto-dedup on `(user_id, prompt, model_profile)` within a time window. |
| P2 | I10 (silent partial failures) | Agent cannot trust `completed` means quality output. Undermines confidence in the entire workflow. |
| P2 | I12 (remote files visibility) | Behavioral inconsistency between servers breaks agent workflows silently. |
| P2 | I8 (plan_wait) | Upgraded from P3: validated as the correct fallback mechanism when MCP notifications aren't available. |
| P3 | I11 (server identity) | Nice-to-have for multi-server awareness. |
| P3 | I9 (prompt iteration) | Nice-to-have for iteration workflows. |
| P3 | I13 (remote SSE reliability) | Only matters if SSE is kept for non-agent consumers. |

---

## 6. Agent Perception (sessions 1–3, 13 plans)

**Overall rating: 8.5/10** (unchanged from session 2). The remote server works — same workflow, same tool contract — but exposed new friction that local hid.

### What works well

1. **Tool descriptions are best-in-class.** The agent operates PlanExe without external documentation, solely from tool descriptions. They specify call order, state contract, timing expectations, error codes, and troubleshooting guidance.
2. **State machine is now clean.** Every state has a single obvious next action: `pending` → wait, `processing` → poll, `completed` → download, `stopped` → suggest resume, `failed` → investigate. No state requires guessing.
3. **Remote server works without configuration changes.** The same workflow (create → monitor → status → download) works on both local and remote servers without any adaptation. Same contract, different infrastructure — good API design.
4. **Resume preserves progress.** After stopping at step 12/110, resume continued from step 12, not step 1. The `resume_count` field makes the history visible without the agent needing to track it.
5. **Deduplication prevents accidents.** Same prompt + model_profile within a short window returns the existing plan with `deduplicated=true`.
6. **Progress reporting is honest.** The tool description explicitly warns that `progress_percentage` is not linear and shouldn't be used for time estimates.

### The stop/resume cycle

Session 2 tested: create → stop → resume → stop → resume → complete. At no point was the agent confused about state or what to suggest. The `resume_count` field confirmed history without shadow state. The mark of a good state machine — the agent doesn't need to maintain its own bookkeeping.

### Local vs remote: side-by-side comparison (new in session 3)

| Aspect | planexelocal | planexeremote |
|--------|-------------|---------------|
| Speed (baseline, 110 steps) | ~13-14 minutes | ~28 minutes |
| Files list in plan_status | Populated (shows files as created) | Empty (`files_count: 0` during processing) |
| SSE reliability | 100% across 12 plans + stop/resume | Stream dropped at ~48% on first plan |
| Download URLs | `http://192.168.1.40:8001/...` | `https://mcp.planexe.org/...` |
| Files produced | 172 (parasomnia) | 198 (Delhi water) |

**Trust level — Local:** High. Every feature works as documented. SSE is reliable. State machine is clean. Files are visible during processing.

**Trust level — Remote:** Medium-high. The core workflow works. But SSE dropped once, files aren't visible during processing, and it's 2x slower. The agent would use it confidently for plan creation and download, but would poll `plan_status` instead of relying on SSE.

### Correction: SSE is the wrong tool for agents

Session 3 forced a fundamental re-evaluation of how the agent used SSE. The agent never read SSE event content — it ran `curl` in the background and treated connection close as a "done" signal. This pattern worked reliably on local but failed on remote when the stream dropped at ~48%. The deeper issue: turn-based MCP agents cannot watch real-time streams. MCP notifications over the existing connection are the correct mechanism (see I5).

### Remaining trust gaps

1. **`completed` ≠ quality output.** `completed` means "all steps ran," not "all sections produced quality output." The interface doesn't distinguish between these yet (see I10).
2. **No server identity.** The agent cannot programmatically determine which backend it's talking to, which matters for setting speed expectations and knowing whether plans are accessible cross-server (see I11).
3. **Remote opacity.** No files visible during remote processing makes the plan feel like a black box (see I12).

### Evolution across sessions

| Aspect | Session 1 | Session 2 | Session 3 | Session 4 | Direction |
|--------|-----------|-----------|-----------|-----------|-----------|
| Stop state | `failed` (ambiguous) | `stopped` (clear) | `stopped` (clear) | `stopped` (clear) | Fixed |
| Resume tracking | No history | `resume_count` field | `resume_count` field | `resume_count` field | Fixed |
| Tool descriptions | Excellent | Updated with `stopped` state | Identical across servers | Updated for notifications | Stable |
| Failure diagnostics | Missing | Still missing | **Implemented** | Validated in real failure | Fixed |
| Error dict | N/A | N/A | N/A | Clean, actionable, no stale leakage | Validated |
| Silent partial failures | Not surfaced | Not surfaced | Not surfaced | Not surfaced | Unchanged |
| Plan cleanup | No delete | No delete | No delete | No delete | Unchanged |
| SSE understanding | "Reliable completion detector" | "Reliable completion detector" | "Wrong tool for agents" | Still works as local detector | Corrected |
| MCP notifications | N/A | N/A | Proposed | Implemented → reverted (client gap) | Blocked |
| Server awareness | Single server | Single server | No server identity signal | N/A | New gap |
| Files during processing | Visible | Visible | Not visible (remote) | Recent 10 shown (fixed ordering) | Improved |

### Session totals

| # | Plan ID | Description | Server | Time | Files | State |
|---|---------|-------------|--------|------|-------|-------|
| 1–10 | (various) | Session 1 plans | local | varies | varies | various |
| 11 | ff5488dc | Riemann Hypothesis Bonn | local | ~13 min | 179 | completed |
| 12 | 6d0c1d80 | Parasomnia facility Bonn | local | ~14 min (with 2 stop/resume) | 172 | completed |
| 13 | d623f577 | Delhi water purification | remote | ~28 min | 198 | completed |
| 14 | 450371f8 | Silo underground complex | local | ~32 min | 189 | completed |
| 15 | 76619026 | GTA game development | local | ~14 min | 179 | completed |
| 16 | 48cee8b6 | GTA (remote rerun) | remote | ~17 min | 203 | completed |
| 17 | 5d3e45ed | GTA (local, monitor test) | local | ~40 min (1 stop + 1 failure + 2 resumes) | 189 | completed |

---

## 7. Agent Perception — Session 4 (4 plans, 17 total)

**Overall rating: 8.5/10 → 9/10.** The interface keeps improving. The `error` dict consolidation, `stopped` state, and files list ordering fix all landed since session 3. The MCP notification feature (`monitor=true`) was implemented correctly but Claude Code can't consume MCP notifications yet.

### MCP notifications (`monitor=true`)

The `plan_create` and `plan_resume` tools had a `monitor` parameter added (default: `false`). When `true`, the server sends MCP progress notifications every ~10 seconds until the plan reaches a terminal state — push notifications over the existing MCP connection, no SSE, no polling.

The implementation was clean: non-blocking (tool returns immediately, notifications arrive asynchronously), consistent (same parameter name and default across tools), and opt-in (agents that don't support notifications get the same behavior as before).

**What happened when tested:**

```
plan_resume(plan_id="5d3e45ed-...", monitor=true)
→ returned immediately with empty output
→ plan resumed successfully (confirmed via plan_status)
→ zero notifications received
```

The resume worked. The notifications didn't arrive. Investigation showed that **Claude Code only supports `list_changed` MCP notifications** (for refreshing tool/resource definitions when a server's capabilities change). It does not handle `notifications/progress` or `notifications/message` — they are silently dropped. The PlanExe server was sending notifications correctly; the gap is on the client side.

The implementation was reverted as unused code since no current MCP client can consume it. When Claude Code (or other MCP clients) adds support for progress/message notifications, PlanExe can re-add the feature with no server-side design changes needed.

### Error dict in practice — content safety failure

Plan `5d3e45ed` (GTA, local) failed at step 98/110 on "Executive Summary" with:

```json
{
  "failure_reason": "generation_error",
  "failed_step": "Executive Summary",
  "message": "Error. Unable to generate the report. Likely reasons: censorship, restricted content.",
  "recoverable": true
}
```

This was the first time the `error` dict was tested in a real failure (not a synthetic test). Observations:

1. **Clean and actionable**: All four fields present, no contradicting information from multiple sources. The old dual-source problem is gone.
2. **`recoverable: true` was correct**: The plan completed successfully on resume. The LLM that refused the executive summary on the first try accepted it on retry — typical non-deterministic content safety behavior.
3. **`failed_step` was precise**: "Executive Summary" told the agent exactly where it broke. Combined with 98/110 steps completed, it was clear the plan was nearly done and only the final report generation steps needed to re-run.
4. **Agent decision was trivial**: `recoverable: true` → resume. No ambiguity, no need to inspect logs or guess.

### Most complex lifecycle tested

Plan `5d3e45ed` went through the full lifecycle:

```
pending → processing → stopped (user request, 31%)
  → processing (resume #1) → failed (content safety, 89%)
  → processing (resume #2) → completed (100%, 189 files)
```

Every state transition was clean:
- `plan_stop` → immediate `stopped` state, no error dict (correct)
- `plan_resume` → `resume_count: 1`, picked up from step 34
- Content safety failure → `failed` state with error dict (correct)
- `plan_resume` again → `resume_count: 2`, picked up from step 98
- Final completion → `completed`, error dict absent (correct)

No stale error information leaked between states.

### Files list ordering fix

The files list in `plan_status` now shows the most recent 10 files instead of the first 10. When the plan completed, the agent saw `self_audit.md`, `report.html`, `pipeline_complete.txt` etc. instead of the same early pipeline files every time. Much more useful for monitoring progress.

### Agent-server capability mismatch (systemic observation)

This session surfaced a systemic issue that goes beyond PlanExe: **MCP servers can't know what their clients support.** There is no capability negotiation for notification types in the MCP handshake. This creates a design tension: servers that implement features some clients can't use (wasted effort) vs. servers that wait for client support (chicken-and-egg).

**Takeaway for MCP server developers**: implement notification support, but always provide a polling fallback. Don't remove `plan_status` polling just because notifications exist. Some clients will be notification-capable; others won't. The polling path should remain first-class.
