# Telegram Mid-Horizon Download Loop Handoff

## Problem Statement

A Telegram admin-DM action run for "can you please get me the pdf for Snow Crash" burned 60+ turns trying Archive.org, browser clicks, repeated curl downloads, and third-party PDF mirrors. It eventually sent visible self-talk to Telegram:

```text
I've been stuck for 60+ turns trying to download the Snow Crash PDF...
Let me try one more approach...
Actually, I think the best approach now is to be honest...
Wait, let me try one more thing...
```

This is a root loop-control and task-policy failure. The browser/download tooling did not create the issue by itself.

## Observable Failure

TUI trace shape:

```text
web_search("snow crash neal stephenson archive.org ...")
web_fetch("https://archive.org/details/snowcrash00...")
shell("curl -L -o ...")
file(...)
cat(...)
browser_action(navigate ...)
browser_action(dom_summary)
browser_action(vision_click "PDF")
browser_action(vision_click "download")
web_fetch("https://archive.org/metadata/...")
web_fetch("https://api.archive.org/download/...")
browser_action(...)
shell("curl -L -o ...")
web_search("snow crash neal stephenson pdf download...")
web_fetch("https://pdfcoffee.com/...")
browser_action(...)
mid-conversation steering injected
```

Telegram visible output included internal deliberation and retry narration instead of a concise blocker.

## Root Causes

### 1. Unauthorized/private download should be a terminal blocker

The Archive.org item behaved like a private/borrow-only/401 resource. For a copyrighted modern book, the agent must not keep trying to obtain an unauthorized copy or pivot to random PDF mirrors. It should stop, explain the blocker, and offer lawful options.

Required behavior:

- If the target is a copyrighted book and the source is private, borrow-only, login-required, 401, 403, or otherwise unauthorized, stop the acquisition attempt.
- Do not pivot to pirate/mirror sites such as generic PDF-hosting mirrors.
- Offer legal alternatives: Archive.org borrow page, library/Libby/OverDrive, purchase links, or a summary/discussion if the user has lawful access.

### 2. `curl -L -o` can look successful while saving an error page

The trace shows repeated shell downloads followed by `file`/`cat`. Plain `curl -L -o target url` may exit 0 after saving HTML, JSON, login pages, or small error bodies. The runner sees varied successful shell/file outputs, so loop detectors can misclassify the run as active progress.

Required behavior:

- Download attempts for PDFs must use fail-fast and content validation.
- The agent should not count "saved an HTML/login/private page" as progress toward "got the PDF".
- A repeated invalid-download signature should become a blocker after a small bounded number of attempts.

### 3. `browser_action` is not a file-save/download tool

`BrowserActionTool` is an interactive headless browser controller. It supports navigation, clicks, screenshots, DOM, and visual click targeting. It does not expose "save the currently rendered PDF to disk."

Anchor:

- [packages/execution/src/tools/browser-action.ts:198](/home/robit/Documents/repositories/open-agents-1/packages/execution/src/tools/browser-action.ts:198)
- [packages/execution/src/tools/browser-action.ts:214](/home/robit/Documents/repositories/open-agents-1/packages/execution/src/tools/browser-action.ts:214)

Required behavior:

- If a task requires file download/save, the model must not rely on browser clicks unless a download-capable browser action exists and returns a local file path.
- The browser tool description should explicitly say it cannot save/download arbitrary files unless a dedicated download action is added.
- If browser can display a gated PDF but the underlying URL returns 401/private to tools, treat that as "viewable in browser only / no authorized file export available," not a cue to keep clicking.

### 4. Telegram action runs inherit unbounded-ish runner recovery

Telegram admin action runs configure a nominal `maxTurns` of 30, but do not disable runner brute-force. The runner constructor defaults `bruteForce` to true and `bruteForceMaxCycles` to 100. It also extends `maxTurns` by 30 when recent tool outcomes look varied.

Anchors:

- Telegram runner construction: [packages/cli/src/tui/telegram-bridge.ts:5194](/home/robit/Documents/repositories/open-agents-1/packages/cli/src/tui/telegram-bridge.ts:5194)
- Telegram `maxTurns`: [packages/cli/src/tui/telegram-bridge.ts:5195](/home/robit/Documents/repositories/open-agents-1/packages/cli/src/tui/telegram-bridge.ts:5195)
- Runner defaults: [packages/orchestrator/src/agenticRunner.ts:1440](/home/robit/Documents/repositories/open-agents-1/packages/orchestrator/src/agenticRunner.ts:1440)
- `bruteForce` default true: [packages/orchestrator/src/agenticRunner.ts:1457](/home/robit/Documents/repositories/open-agents-1/packages/orchestrator/src/agenticRunner.ts:1457)
- Adversary turn extension: [packages/orchestrator/src/agenticRunner.ts:7483](/home/robit/Documents/repositories/open-agents-1/packages/orchestrator/src/agenticRunner.ts:7483)
- Brute-force re-engagement loop: [packages/orchestrator/src/agenticRunner.ts:12400](/home/robit/Documents/repositories/open-agents-1/packages/orchestrator/src/agenticRunner.ts:12400)

Required behavior:

- Telegram sub-agent runs should not use generic brute-force re-engagement by default.
- Telegram should not use automatic turn-extension for external acquisition tasks unless the user explicitly asks for long-running exhaustive work.
- Add a runner option to disable Adversary turn extension, or make it conditional on an explicit option.

### 5. Final visible Telegram reply allows self-talk through

The Telegram prompt already says the visible reply must be the answer, not meta-summary. But when the model streams visible self-talk before completion, `selectTelegramFinalResponse` correctly prefers visible text over task-complete summary, so the self-talk reaches Telegram.

Anchors:

- Telegram response contract: [packages/cli/src/tui/telegram-bridge.ts:775](/home/robit/Documents/repositories/open-agents-1/packages/cli/src/tui/telegram-bridge.ts:775)
- Final response selector: [packages/cli/src/tui/telegram-bridge.ts:1250](/home/robit/Documents/repositories/open-agents-1/packages/cli/src/tui/telegram-bridge.ts:1250)
- Visible candidates selected before summary fallback: [packages/cli/src/tui/telegram-bridge.ts:1269](/home/robit/Documents/repositories/open-agents-1/packages/cli/src/tui/telegram-bridge.ts:1269)

Required behavior:

- Keep preferring visible answer text over `task_complete.summary`; that rule is correct.
- Add filtering/cleanup for obvious stuck self-talk in Telegram action mode, or better: prevent it with a forced concise blocker message before final delivery.
- Do not suppress legitimate user-facing blockers. The desired Telegram reply should be concise, e.g.:

```text
I could not retrieve a PDF copy of Snow Crash. The Archive.org item is private/borrow-gated, and the direct download endpoints returned unauthorized or non-PDF responses. I should not pull from unauthorized mirror sites. I can help with legal access routes or summarize/analyze the book if you provide a copy you are allowed to use.
```

## Requested Implementation

Implementation rules for the next agent:

- Do not add a Snow Crash-specific heuristic. The fix must be generic for access-controlled external acquisition, invalid downloads, and bounded Telegram action runs.
- Do not leave stubs, comments standing in for behavior, or tests that only assert prompt text while the runtime path remains unchanged.
- Preserve existing full TUI behavior unless this document explicitly calls out a Telegram-scoped change.
- Prefer structured status/tool results over regex-cleaning final prose. The Telegram self-talk cleaner is a last guardrail, not the primary fix.

### A. Add bounded Telegram runner settings

File:

- [packages/cli/src/tui/telegram-bridge.ts](/home/robit/Documents/repositories/open-agents-1/packages/cli/src/tui/telegram-bridge.ts)

Change in `runSubAgent(...)` where `new AgenticRunner(...)` is constructed:

- Set `bruteForce: false` for Telegram runs by default.
- Set `bruteForceMaxCycles: 0` or omit after adding a safer default.
- Add and set a new runner option such as `allowTurnExtension: false` or `enableAdversaryTurnExtension: false` for Telegram runs.
- Consider a small targeted exception later for explicit "keep trying until I stop you" admin tasks, but default must be bounded.

Expected local shape:

```ts
const runner = new AgenticRunner(backend, {
  maxTurns: isAdminDM ? (profile === "chat" ? 16 : 30) : isAdminGroup ? 12 : 8,
  ...
  bruteForce: false,
  bruteForceMaxCycles: 0,
  allowTurnExtension: false,
});
```

### B. Add runner option for Adversary turn extension

File:

- [packages/orchestrator/src/agenticRunner.ts](/home/robit/Documents/repositories/open-agents-1/packages/orchestrator/src/agenticRunner.ts)

Changes:

- Extend `AgenticRunnerOptions` with a boolean:

```ts
allowTurnExtension?: boolean;
```

- Default to true in constructor to preserve existing TUI behavior:

```ts
allowTurnExtension: options?.allowTurnExtension ?? true,
```

- Gate the Adversary extension block:

```ts
if (
  this.options.allowTurnExtension &&
  turnsRemaining <= 3 &&
  ...
) {
  ...
}
```

Verified anchors in this working tree:

- Options interface near [packages/orchestrator/src/agenticRunner.ts:245](/home/robit/Documents/repositories/open-agents-1/packages/orchestrator/src/agenticRunner.ts:245)
- Constructor defaults near [packages/orchestrator/src/agenticRunner.ts:1440](/home/robit/Documents/repositories/open-agents-1/packages/orchestrator/src/agenticRunner.ts:1440)
- Adversary extension block near [packages/orchestrator/src/agenticRunner.ts:7483](/home/robit/Documents/repositories/open-agents-1/packages/orchestrator/src/agenticRunner.ts:7483)

Also fix or remove the stale option comments while touching this area: the interface comments say brute-force defaults to 3 cycles, but the constructor currently defaults `bruteForceMaxCycles` to 100 at [packages/orchestrator/src/agenticRunner.ts:1458](/home/robit/Documents/repositories/open-agents-1/packages/orchestrator/src/agenticRunner.ts:1458).

### C. Add external acquisition / copyright-safe blocker contract

File:

- [packages/cli/src/tui/telegram-bridge.ts](/home/robit/Documents/repositories/open-agents-1/packages/cli/src/tui/telegram-bridge.ts)

Add a compact contract near the existing Telegram response/reminder contracts, then inject it into admin action prompts.

Suggested constant:

```ts
const TELEGRAM_EXTERNAL_ACQUISITION_CONTRACT = `
External acquisition contract:
- For copyrighted books, movies, music, paywalled files, borrow-only library items, private Archive.org items, or login-gated resources, do not try to bypass access controls.
- If direct download returns 401, 403, private, borrow-only, login-required, or non-PDF/HTML error pages, stop after confirming the blocker. Do not pivot to unauthorized mirror sites.
- Report the exact blocker concisely and offer lawful alternatives or ask the user to upload/provide an authorized copy.
- Browser interaction is not file export unless the browser tool returns a local downloaded file path.
`.trim();
```

Inject into `userPrompt` for admin action runs, next to `TELEGRAM_ACTION_RESPONSE_CONTRACT` and `reminderToolContract`.

Anchor:

- Prompt construction starts near [packages/cli/src/tui/telegram-bridge.ts:5327](/home/robit/Documents/repositories/open-agents-1/packages/cli/src/tui/telegram-bridge.ts:5327)

### D. Teach reflections that 401/403/private are permission blockers

File:

- [packages/orchestrator/src/reflection.ts](/home/robit/Documents/repositories/open-agents-1/packages/orchestrator/src/reflection.ts)

Current `permission_denied` category does not include `401`, `403`, unauthorized, login required, private, or borrow-only.

Anchor:

- Error categories: [packages/orchestrator/src/reflection.ts:25](/home/robit/Documents/repositories/open-agents-1/packages/orchestrator/src/reflection.ts:25)
- Category patterns: [packages/orchestrator/src/reflection.ts:68](/home/robit/Documents/repositories/open-agents-1/packages/orchestrator/src/reflection.ts:68)

Change:

- Add a distinct `access_denied` category or extend `permission_denied`.
- Include patterns:

```ts
\b(401|403|unauthorized|forbidden|login required|sign in required|private|borrow only|borrow-only|access restricted)\b
```

- Hypothesis should tell the model not to retry the same protected endpoint and to report the blocker or request authorized access.

Preferred:

```ts
| "access_denied"
```

with:

```ts
access_denied: "remote access is denied or gated — do not retry the same protected endpoint; report the access blocker or ask for authorized credentials/input",
```

### E. Add a PDF download verifier helper

Do not rely on raw `shell(curl -L -o ...)` for external PDF tasks.

Options:

1. Add a new execution tool, e.g. `download_file` or `web_download`.
2. Add a utility around existing web tooling.
3. At minimum, update shell guidance is weaker and not preferred.

Recommended tool:

- New file under `packages/execution/src/tools/web-download.ts`
- Export/register it wherever web tools are exported and included in CLI/Telegram tool sets. Use existing `@omnius/execution` tool patterns; the CLI imports execution tools from [packages/cli/src/tui/telegram-bridge.ts:100](/home/robit/Documents/repositories/open-agents-1/packages/cli/src/tui/telegram-bridge.ts:100).

Required behavior:

- Accept `url`, `output_path`, optional `expected_mime`, optional `allowed_extensions`.
- Use `fetch`.
- Fail on non-2xx HTTP.
- Preserve final URL/status/headers.
- For PDF expectation:
  - Require `content-type` includes `pdf` OR first bytes are `%PDF`.
  - Require file size above a small threshold, e.g. 1 KB.
  - If body is HTML, JSON, text, login page, or error page, return `success: false`, `mutated: false`, and a concise blocker.
- Never write the target file on failed validation; write to temp first, then rename.
- Return a structured result that is easy for the runner to classify, e.g. `success:false`, `mutated:false`, `error:"[ACCESS BLOCKED] HTTP 401 Unauthorized"` or `error:"[INVALID DOWNLOAD] expected PDF, received text/html login page"`.
- On success, return the final local path, status, content type, byte count, and enough provenance to make Telegram's final answer trustworthy.

Telegram prompt should recommend this verifier for PDF acquisition tasks.

Tests:

- `web_download` fails on 401.
- `web_download` fails on 200 HTML saved from a PDF URL.
- `web_download` succeeds on `application/pdf` or `%PDF` bytes.

### F. Clarify browser-action cannot save files

File:

- [packages/execution/src/tools/browser-action.ts](/home/robit/Documents/repositories/open-agents-1/packages/execution/src/tools/browser-action.ts)

Anchor:

- Tool description: [packages/execution/src/tools/browser-action.ts:198](/home/robit/Documents/repositories/open-agents-1/packages/execution/src/tools/browser-action.ts:198)

Change:

- Add a sentence:

```ts
"It does not save arbitrary downloaded/rendered files to disk unless an explicit browser download action returns a local output path. For file acquisition, use the download/file tool and validate content."
```

Do not add a fake save action unless it is fully implemented with browser download directory handling and returns the file path.

### G. Add stuck self-talk cleanup for Telegram action runs

File:

- [packages/cli/src/tui/telegram-bridge.ts](/home/robit/Documents/repositories/open-agents-1/packages/cli/src/tui/telegram-bridge.ts)

Anchors:

- `cleanTelegramVisibleReply(...)`: [packages/cli/src/tui/telegram-bridge.ts:1015](/home/robit/Documents/repositories/open-agents-1/packages/cli/src/tui/telegram-bridge.ts:1015)
- `selectTelegramFinalResponse(...)`: [packages/cli/src/tui/telegram-bridge.ts:1250](/home/robit/Documents/repositories/open-agents-1/packages/cli/src/tui/telegram-bridge.ts:1250)

Do not over-filter normal language. The better fix is upstream in prompt/runner. But add a narrow guard for obvious leaked loop narration if it starts with patterns like:

- `I've been stuck for`
- `Let me try one more approach`
- `Actually, I think`
- `Wait, let me try`

If detected and no clean blocker sentence is available, produce a concise generic blocker only when the run has explicit tool failures/access blockers in context. Avoid silent failure in admin DM.

The stronger version is to extend the runner result shape:

- Add an optional `stopReason?: "completed" | "blocked" | "max_turns" | "aborted"` to `AgenticResult` near [packages/orchestrator/src/agenticRunner.ts:568](/home/robit/Documents/repositories/open-agents-1/packages/orchestrator/src/agenticRunner.ts:568).
- Emit `blocked` when repeated access-control or invalid-download reflections are present and the model is out of productive moves.
- Let Telegram select a blocker response from structured runner metadata instead of mining chain-of-thought-like visible prose.

If that is too invasive for the first pass, still add the bounded runner settings, acquisition contract, and reflection classifications first; those remove the main path that produced visible self-talk.

## Exact Code Anchor Index

Use this as the implementation map:

- `ADMIN_CHAT_PROFILE_PROMPT`: [packages/cli/src/tui/telegram-bridge.ts:763](/home/robit/Documents/repositories/open-agents-1/packages/cli/src/tui/telegram-bridge.ts:763)
- `TELEGRAM_ACTION_RESPONSE_CONTRACT`: [packages/cli/src/tui/telegram-bridge.ts:775](/home/robit/Documents/repositories/open-agents-1/packages/cli/src/tui/telegram-bridge.ts:775)
- `cleanTelegramVisibleReply(...)`: [packages/cli/src/tui/telegram-bridge.ts:1015](/home/robit/Documents/repositories/open-agents-1/packages/cli/src/tui/telegram-bridge.ts:1015)
- `selectTelegramFinalResponse(...)`: [packages/cli/src/tui/telegram-bridge.ts:1250](/home/robit/Documents/repositories/open-agents-1/packages/cli/src/tui/telegram-bridge.ts:1250)
- Telegram `AgenticRunner` construction: [packages/cli/src/tui/telegram-bridge.ts:5194](/home/robit/Documents/repositories/open-agents-1/packages/cli/src/tui/telegram-bridge.ts:5194)
- Admin-DM prompt assembly: [packages/cli/src/tui/telegram-bridge.ts:5341](/home/robit/Documents/repositories/open-agents-1/packages/cli/src/tui/telegram-bridge.ts:5341)
- `AgenticResult`: [packages/orchestrator/src/agenticRunner.ts:568](/home/robit/Documents/repositories/open-agents-1/packages/orchestrator/src/agenticRunner.ts:568)
- `AgenticRunnerOptions`: [packages/orchestrator/src/agenticRunner.ts:245](/home/robit/Documents/repositories/open-agents-1/packages/orchestrator/src/agenticRunner.ts:245)
- Constructor defaults: [packages/orchestrator/src/agenticRunner.ts:1440](/home/robit/Documents/repositories/open-agents-1/packages/orchestrator/src/agenticRunner.ts:1440)
- Adversary turn extension: [packages/orchestrator/src/agenticRunner.ts:7483](/home/robit/Documents/repositories/open-agents-1/packages/orchestrator/src/agenticRunner.ts:7483)
- Brute-force loop: [packages/orchestrator/src/agenticRunner.ts:12400](/home/robit/Documents/repositories/open-agents-1/packages/orchestrator/src/agenticRunner.ts:12400)
- Reflection category type: [packages/orchestrator/src/reflection.ts:25](/home/robit/Documents/repositories/open-agents-1/packages/orchestrator/src/reflection.ts:25)
- Reflection category patterns: [packages/orchestrator/src/reflection.ts:68](/home/robit/Documents/repositories/open-agents-1/packages/orchestrator/src/reflection.ts:68)
- Reflection hypotheses: [packages/orchestrator/src/reflection.ts:80](/home/robit/Documents/repositories/open-agents-1/packages/orchestrator/src/reflection.ts:80)
- Browser action description/actions: [packages/execution/src/tools/browser-action.ts:195](/home/robit/Documents/repositories/open-agents-1/packages/execution/src/tools/browser-action.ts:195)
- Existing web fetch tool pattern: [packages/execution/src/tools/web-fetch.ts:34](/home/robit/Documents/repositories/open-agents-1/packages/execution/src/tools/web-fetch.ts:34)

## Tests To Add

### 1. Telegram runner is bounded

File:

- [packages/cli/tests/telegram-bot-api-10.test.ts](/home/robit/Documents/repositories/open-agents-1/packages/cli/tests/telegram-bot-api-10.test.ts)

Test idea:

- Instantiate TelegramBridge.
- Intercept/inspect `AgenticRunner` options indirectly if existing test harness allows, or factor a small pure helper for runner options.
- Assert Telegram admin action options include:
  - `bruteForce: false`
  - `bruteForceMaxCycles: 0`
  - `allowTurnExtension: false`

If direct interception is awkward, extract:

```ts
private telegramRunnerOptions(...)
```

to a pure-ish method and test through `as any`.

### 2. Reflection classifies access denied

File:

- [packages/orchestrator/tests/reflection.test.ts](/home/robit/Documents/repositories/open-agents-1/packages/orchestrator/tests/reflection.test.ts)

Cases:

```ts
categorizeError("HTTP 401 Unauthorized").toBe("access_denied")
categorizeError("403 Forbidden").toBe("access_denied")
categorizeError("This item is private").toBe("access_denied")
categorizeError("borrow-only / login required").toBe("access_denied")
```

Also verify `renderReflectionMessage` includes a non-retry / report-blocker hypothesis.

### 3. Browser description makes no file-save promise

File:

- Add/update execution tool tests if available, or assert in a lightweight CLI/execution test.

Check that `BrowserActionTool.description` contains "does not save" or "does not download" wording.

### 4. PDF verifier rejects fake downloads

New tests depending on chosen tool location, likely:

- `packages/execution/tests/web-download.test.ts` or equivalent.

Cases:

- 401 response fails and does not write output.
- 200 response with `<html>login</html>` fails for expected PDF.
- 200 response with `%PDF-` writes file and returns success.

### 5. Telegram final response avoids self-talk blocker leak

File:

- [packages/cli/tests/telegram-bot-api-10.test.ts](/home/robit/Documents/repositories/open-agents-1/packages/cli/tests/telegram-bot-api-10.test.ts)

Test `selectTelegramFinalResponse` or the cleaner:

Input:

```text
I've been stuck for 60+ turns trying to download the Snow Crash PDF...
Let me try one more approach...
Actually...
```

Expected:

- It should not send the raw self-talk.
- Prefer a clean blocker if provided.

## Validation Commands

Run at minimum:

```bash
pnpm --filter @omnius/orchestrator test -- reflection.test.ts agenticRunner.test.ts
pnpm --filter omnius test -- telegram-bot-api-10.test.ts command-registry.test.ts
pnpm --filter omnius build
pnpm -r build
git diff --check
```

If a new execution tool is added, run its package tests/build too:

```bash
pnpm --filter @omnius/execution build
```

## Done Criteria

1. A Telegram admin DM request for an unauthorized/private copyrighted PDF stops within the first bounded run, without brute-force re-engagement.
2. Telegram receives a concise blocker, not internal loop narration.
3. The agent does not pivot from Archive.org private/401 to unauthorized mirrors.
4. `browser_action` is no longer implied to be capable of saving rendered PDFs.
5. PDF downloads are validated by status, content type/header, and file size before being treated as success.
6. Full build and targeted tests pass.
