Skip to content

fix(agent): keep one cause one streak class in the media envelopes - #502

Merged
0xKT merged 3 commits into
mainfrom
fix/media_envelope_streak_class
Sep 19, 2026
Merged

0xKT merged 3 commits into
mainfrom
fix/media_envelope_streak_class

Conversation

@ZuyiZhou

@ZuyiZhou ZuyiZhou commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Summary

failure_class keys the loop's tool-failure streak on an envelope's error
string alone. That works only while the string comes from the small fixed
vocabulary its producers are meant to keep, and it is a convention the module
cannot enforce - the docstring added in #410 names exactly this failure mode:
one cause counting as many.

Every media tool broke it. _format_http_error interpolated up to 400
characters of vendor body (which carries a request id) into error; the
unreadable-reference handlers interpolated the path; the four catch-alls put
str(e) there, which spells the host. So a dead endpoint produced a class per
call, (tool, failure_class) never repeated, the streak never reached
_LOOP_BREAK_THRESHOLD, and the stop-repeating nudge was unreachable for
image_generate, text_to_speech and video_generate alike.

The URL gate in front of web_fetch had it too, in both copies of the tool:
most reasons validate_url_target composes name the address they refused, so
a model walking an internal range got one class per address. #410 fixed the
handlers behind that gate and left the gate itself.

error now carries only what a reader could enumerate. The variable part moves
to detail, which keeps it in front of the model and inside
is_hard_tool_failure's transient-marker scan. Where the split gave the single
call a further key, the batch reducer forwards that too: hint, which names the
proxy a 403 can be routed through, and url, which names the reference a
guarded fetch refused. A batched picture is told what the single call is told,
which is the property the split has to preserve. Distinct causes stay distinct,
which is the failure mode this trades against: two HTTP statuses, two exception
types and the four vendor refusals each keep their own class, and the tests
assert that direction too.

Found by sweeping for the shape rather than by reading the code: every
"error": whose value is not a literal is a candidate, and that grep is the
cheapest guard against this recurring.

Type

  • Fix
  • Feature
  • Docs
  • CI / tooling
  • Refactor
  • Other

Verification

  • uv run pytest tests/ -q -p no:randomly -> 23443 passed, 109 skipped

  • make lint-python -> ruff check and format clean

  • make lint-types -> all checks passed

  • make lint-imports -> 10 contracts kept, 0 broken

  • make check-source-language, make check-large-files -> clean

  • Revert-to-red, per fix, restoring the source from a backup afterwards:
    reverting media_gen.py alone turns the four new class tests red and leaves
    test_two_different_statuses_stay_two_classes green; reverting either copy
    of web.py turns only that copy's gate test red and leaves the two feat(*): dead-end predicates, verbatim tool bodies and envelope failures #410
    streak tests green. For the batch fold, each key is pinned on its own:
    reverting it to detail alone turns both new batch tests red, restoring
    hint alone leaves the reference test red, restoring url alone leaves the
    403 test red.

  • After rebase onto 45203a4: uv run pytest tests/test_media_gen_tool.py tests/test_security_web_ssrf.py -q -p no:randomly -> 100 passed;
    make lint-python, make lint-types, make check-source-language,
    make check-large-files -> clean.

  • Relevant tests pass locally

  • Relevant lint / type checks pass locally

  • User-facing docs or screenshots are updated when needed

Risk

Tool output shape changes: a caller reading the reason out of error now
finds it in detail. The readers in this repo are the model itself and the
tests updated here; tests/test_security_web_ssrf.py is the one behavioural
assertion that moved, and it still pins that a private address is refused for
being private. Rollback is a plain revert of this commit - no state, no
migration, no config.

  • Security impact considered
  • Backward compatibility considered
  • Rollback path is clear for risky changes

Related Issues

N/A. Finishes the envelope work started in #410, which fixed the reader's
handlers and left the gate in front of them and every media tool.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: batched media HTTP 403 failures lose their proxy recovery hint.

I found one inline blocker. The stable error/detail split otherwise preserves the intended streak behavior and keeps diagnostics model-visible.

Covered: the full diff; surrounding envelope consumers and the agent-loop caller; relevant history; backward compatibility of the JSON envelopes; whether tests were weakened; and the AGENTS.md, CLAUDE.md, CONTEXT-MAP.md, and Runtime architecture constraints.

Verification: the affected suite passed with 307 tests and 17 warnings. The direct batch-reducer reproduction confirmed the missing hint. The source-language gate passed. The full suite did not pass in this checkout: 18,312 passed, 126 skipped, 67 failed, and 235 errored; the output includes missing optional/dev components such as raven_ppt, raven_design, raven_everos, and websockets.

Comment thread raven/agent/tools/media_gen.py Resolved

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: the batch reducer still drops the HTTP 403 recovery hint.

The new revision adds useful coverage for the images, speech, and video routes, but it does not change raven/agent/tools/media_gen.py:609: batch results still omit the hint field introduced by _format_http_error. The existing inline finding remains current, so I have not duplicated it or resolved its thread.

I reviewed the new delta and rechecked the full branch diff, affected callers, relevant history, backward compatibility, test strength, and the repository and Runtime architecture constraints. I found no additional issues.

Verification: uv run pytest tests/test_media_gen_tool.py -q passed with 95 tests. uv run python scripts/check_source_language.py github/main...HEAD passed. Current GitHub checks are otherwise green or still in progress.

ZuyiZhou and others added 3 commits September 18, 2026 22:58
failure_class keys the loop's tool-failure streak on an envelope's "error"
string alone, which holds only while that string comes from the small fixed
vocabulary its producers are meant to keep. The media tools interpolated the
vendor body, the unreadable reference path and the exception's own text into
it, and the URL gate in front of both copies of the reader interpolated the
address it refused. One dead endpoint, one bad reference or one blocked range
therefore produced a class per call, the streak never reached the break
threshold, and the stop-repeating nudge was unreachable for every tool here.

"error" now carries only what a reader could enumerate; the variable part
moves to "detail", which keeps it in front of the model and inside
is_hard_tool_failure's transient-marker scan. A batch result forwards
"detail" so a per-picture reason is not lost with it.

Distinct causes stay distinct, which is the failure mode this trades against:
two HTTP statuses, two exception types and the four vendor refusals each keep
their own class.

Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
…r takes

The diff-coverage gate named four lines: a dedicated image model goes to the
Images API and never touches chat/completions, so that route carries its own
copies of both handlers, and speech and video each carry their own catch-all.
The streak key is (tool, failure_class), so each of those tools accumulates
its own streak and each one could lose the nudge on its own.

Each new test makes the same claim as the ones it joins rather than only
walking the line: two failures from one cause are one class, and the host or
path that varies is still in front of the model under "detail".

Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Splitting "error" into a class plus the variable part gave the single call two
new keys: "hint", where a 403 names the proxy the media calls can be routed
through, and "url", where a guarded fetch names the reference it refused. The
batch reducer was taught to forward "detail" and not those two, so the same
403 answered a batched picture with the status and the vendor body alone while
the single call still told the operator how to get past it, and a refused
reference reached the model with nothing saying which address was refused.

Both keys now travel with "error" out of the fold. The batched picture is told
what the single call is told, which is the property the split is supposed to
preserve.

Each test pins one key: reverting the fold to "detail" alone turns both red,
restoring "hint" alone leaves the reference test red, and restoring "url"
alone leaves the 403 test red.

Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
@ZuyiZhou
ZuyiZhou force-pushed the fix/media_envelope_streak_class branch from ca1afc9 to b24eb75 Compare September 18, 2026 14:58
@ZuyiZhou
ZuyiZhou requested a review from LivXue as a code owner September 18, 2026 14:58

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

The revised batch fold preserves every field split out of error by this change: detail, the 403 recovery hint, and the refused-reference url. The new tests cover the real batched call paths, and my prior blocker is fixed and resolved. I found no new issues.

Covered: the current full diff and revision delta; media-envelope producers, the batch consumer, and failure-streak caller; relevant history; backward compatibility; test strength; and the AGENTS.md, CLAUDE.md, CONTEXT-MAP.md, and Runtime architecture constraints.

Verification: uv run pytest tests/test_agent_loop_tool_loop_break.py tests/test_agent_tools_web_providers.py tests/test_agents_research_tools.py tests/test_media_gen_tool.py tests/test_security_web_ssrf.py -q passed with 312 tests and 17 warnings. uv run python scripts/check_source_language.py github/main...HEAD passed. GitHub checks are green or still in progress.

@0xKT

0xKT commented Sep 18, 2026

Copy link
Copy Markdown
Member

Not a blocker -- the model-facing envelope is unaffected. This is about the row that lands on disk.

_fetch_record builds the fetch ledger row by copying a fixed key list off the tool's own
envelope (agents/raven-research/plugins/research-flow/research_flow/tools/web.py:1745-1757).
That list carries ("error", "error") at 1756 and no detail. This PR moves the refusal
reason out of error and into detail at web.py:1828, so the reason no longer reaches the row.

Measured at the seam, handing the real _fetch_record one envelope each:

HEAD envelope: {"error": "URL validation failed",
                "detail": "blocked: private/internal address 127.0.0.1", "url": ...}
  row -> error='URL validation failed', no detail key, nothing in the row names the address

BASE envelope: {"error": "URL validation failed: blocked: private/internal address 127.0.0.1",
                "url": ...}
  row -> error='URL validation failed: blocked: private/internal address 127.0.0.1'

Same function, same call; only the envelope differs.

The row is written on the production path, not a window built for the probe:
_ledger_append(self._fetch_record(url, out)) at web.py:1698-1699.

Why this is more than cosmetic: _error_shaping in this same file (web.py:374-381) records what
the ledger's error column is for -- "transport_err: true alone made a drained API key and a
network blip the same row - during the one mid-run outage this project has already eaten, the
ledger could not say which it was. The error string and status make the row diagnosable." After
this change every refused target writes the same undiagnosable row.

Scope, in fairness to the change: the sibling handlers at 1852 and 1865 split the same way before
this PR, so the copy list was already behind them; this PR widens the gap to the validation gate.
And the split itself is right -- the reason names the host, which is exactly the "one cause, many
classes" shape this PR exists to remove. What is missing is the reader update, which this PR did
perform for the media batch fold (raven/agent/tools/media_gen.py:604-613) and not here.

One entry in that tuple closes it: ("detail", "detail").

@gloryfromca

Copy link
Copy Markdown
Member

No blockers; suggestions only, and they are marked inline.

Confirmed at the production seam: _fetch_record keeps error but drops the new detail, so refused targets now persist as identical URL validation failed rows with no address-specific reason. This does not affect the model-facing envelope or mergeability, but it makes my earlier clean stance nonblocking. I am not duplicating the verifier note or its one-entry fix.

Verification: the direct _fetch_record reproduction omitted detail, and uv run pytest tests/test_agents_research_tools.py tests/test_agents_research_verbatim_sink.py -q passed with 98 tests.

@0xKT
0xKT merged commit 63ab3f4 into main Sep 19, 2026
26 checks passed
@0xKT
0xKT deleted the fix/media_envelope_streak_class branch September 19, 2026 12:23
LivXue pushed a commit that referenced this pull request Oct 12, 2026
)

## Summary

`failure_class` keys the loop's tool-failure streak on an envelope's
`error`
string alone. That works only while the string comes from the small
fixed
vocabulary its producers are meant to keep, and it is a convention the
module
cannot enforce - the docstring added in #410 names exactly this failure
mode:
one cause counting as many.

Every media tool broke it. `_format_http_error` interpolated up to 400
characters of vendor body (which carries a request id) into `error`; the
unreadable-reference handlers interpolated the path; the four catch-alls
put
`str(e)` there, which spells the host. So a dead endpoint produced a
class per
call, `(tool, failure_class)` never repeated, the streak never reached
`_LOOP_BREAK_THRESHOLD`, and the stop-repeating nudge was unreachable
for
`image_generate`, `text_to_speech` and `video_generate` alike.

The URL gate in front of `web_fetch` had it too, in both copies of the
tool:
most reasons `validate_url_target` composes name the address they
refused, so
a model walking an internal range got one class per address. #410 fixed
the
handlers behind that gate and left the gate itself.

`error` now carries only what a reader could enumerate. The variable
part moves
to `detail`, which keeps it in front of the model and inside
`is_hard_tool_failure`'s transient-marker scan. Where the split gave the
single
call a further key, the batch reducer forwards that too: `hint`, which
names the
proxy a 403 can be routed through, and `url`, which names the reference
a
guarded fetch refused. A batched picture is told what the single call is
told,
which is the property the split has to preserve. Distinct causes stay
distinct,
which is the failure mode this trades against: two HTTP statuses, two
exception
types and the four vendor refusals each keep their own class, and the
tests
assert that direction too.

Found by sweeping for the shape rather than by reading the code: every
`"error":` whose value is not a literal is a candidate, and that grep is
the
cheapest guard against this recurring.

## Type

- [x] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

- `uv run pytest tests/ -q -p no:randomly` -> 23443 passed, 109 skipped
- `make lint-python` -> ruff check and format clean
- `make lint-types` -> all checks passed
- `make lint-imports` -> 10 contracts kept, 0 broken
- `make check-source-language`, `make check-large-files` -> clean
- Revert-to-red, per fix, restoring the source from a backup afterwards:
reverting `media_gen.py` alone turns the four new class tests red and
leaves
`test_two_different_statuses_stay_two_classes` green; reverting either
copy
of `web.py` turns only that copy's gate test red and leaves the two #410
  streak tests green. For the batch fold, each key is pinned on its own:
reverting it to `detail` alone turns both new batch tests red, restoring
`hint` alone leaves the reference test red, restoring `url` alone leaves
the
  403 test red.
- After rebase onto 45203a4: `uv run pytest
tests/test_media_gen_tool.py
  tests/test_security_web_ssrf.py -q -p no:randomly` -> 100 passed;
  `make lint-python`, `make lint-types`, `make check-source-language`,
  `make check-large-files` -> clean.

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [ ] User-facing docs or screenshots are updated when needed

## Risk

Tool output shape changes: a caller reading the reason out of `error`
now
finds it in `detail`. The readers in this repo are the model itself and
the
tests updated here; `tests/test_security_web_ssrf.py` is the one
behavioural
assertion that moved, and it still pins that a private address is
refused for
being private. Rollback is a plain revert of this commit - no state, no
migration, no config.

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

## Related Issues

N/A. Finishes the envelope work started in #410, which fixed the
reader's
handlers and left the gate in front of them and every media tool.

---------

Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants