Skip to content

fix(experts): report an unsupported avatar delete instead of a 500 - #1297

Open
lihongyuan99 wants to merge 5 commits into
TencentCloud:developfrom
lihongyuan99:fix/avatar-delete-unsupported
Open

lihongyuan99 wants to merge 5 commits into
TencentCloud:developfrom
lihongyuan99:fix/avatar-delete-unsupported

Conversation

@lihongyuan99

@lihongyuan99 lihongyuan99 commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

delete_workspace_avatar() treats OSError / FileNotFoundError as "nothing to remove", but the
harness has a third, documented outcome for a backend that has no delete primitive:
BackendOperationNotSupportedError (octop_harness/backends/workspace.py:973-974). It subclasses
RuntimeError, not OSError, so it escapes the loop and reaches the router:

  • POST /api/agents/{agent_id}/avatar — write_workspace_avatar() deletes the other extensions
    before uploading (src/octop/infra/agents/experts/avatar.py:97), so an upload can never land.
  • DELETE /api/agents/{agent_id}/avatar — same uncaught exception.

Both answer 500 with a bare traceback instead of a structured OctopError.

It is reachable on a backend Octop ships

OpenSandbox is selectable as {"type": "opensandbox"} and Octop installs its dependency on
demand (src/octop/infra/agents/manager.py:2667-2714). Capability probe against the
octop-harness pinned in this repo's venv:

OpenSandbox     has delete_path: False | has adelete_path: False | cwd: False | root_dir: False
DockerSandbox   has delete_path: True  | mkdir_path / move_path present
S3Backend       has delete_path: True  | adelete_path: True

For OpenSandbox, sandbox_fs=True makes _has_backend_mount() return False
(workspace.py:315-316) and virtual_mode=True keeps _mutation_storage_key() non-None, so
delete() reaches the raise. With the real classes (the optional opensandbox package is not
needed for the delete path), on develop 4667bd7b:

ws = BackendWorkspace(<OpenSandbox instance, sandbox_fs=True, virtual_mode=True>, tmp_path)
mutation key: /.octop/avatar.png
ws.adelete -> BackendOperationNotSupportedError | delete is not supported by backend type 'OpenSandbox'
write_workspace_avatar -> BackendOperationNotSupportedError
delete_workspace_avatar -> BackendOperationNotSupportedError

DockerSandbox and the cloud backends are unaffected because they implement delete_path — which
is why this stayed invisible.

Fix

Map the condition to the code the repository already reserves for it, instead of inventing a new
one: OctopError(ErrorCode.WORKSPACE_OP_UNSUPPORTED, str(exc)) → HTTP 400
(api/routers/workspace.py:56-58 _map_workspace_fs_error does exactly this for the workspace
routes; infra/errors.py:188 carries the status). No backend-type branching, per
AGENTS.md §5 — the harness error is the signal.

Not suppressing it like mkdir one line above (avatar.py:100) is deliberate: the delete loop
enforces "one avatar file per agent", and read_workspace_avatar() scans .octop/avatar.png
first. Skipping the failure would let aupload_bytes() write avatar.jpg next to a stale
avatar.png and return 201 while every client keeps reading the old image. A 400 keeps the
contract honest and writes nothing.

Target branch

  • Base is develop (feature / fix — default)
  • Base is main (release/* or hotfix/* only)

Type of change

  • Bug fix

Test plan

New unit cases (tests/unit/agents/test_agent_avatar.py) reuse the file's existing
_MemoryWorkspace, so they need no optional dependency and no network:
test_delete_maps_unsupported_primitive_to_client_error and
test_write_refuses_when_avatar_cannot_be_replaced (the latter also pins that a refused upload
leaves no half-written file).

Red before the fix, green after:

$ uv run pytest tests/unit/agents/test_agent_avatar.py -q            # develop 4667bd7b + new tests
2 failed, 7 passed in 1.54s
  BackendOperationNotSupportedError: delete is not supported by backend type 'OpenSandbox'

$ uv run pytest tests/unit/agents/test_agent_avatar.py -q            # with the fix
9 passed in 1.61s
  • Added/updated tests
  • make all passes locally — not run verbatim: make all starts with format-all, which
    runs Prettier over dashboard/, and needs make install-hooks; this clone has no
    dashboard/node_modules and the PR touches no frontend file. The three backend gates it covers
    were run by hand on this working tree instead:
$ uv run ruff check src tests                     All checks passed!
$ uv run ruff format --check src tests            1080 files already formatted
$ uv run mypy src/octop                           Success: no issues found in 530 source files

Full suite (-n 6 for the local run, no LLM calls):

$ uv run pytest -q -m "not live"                  3849 passed, 17 skipped in 222.00s

3847 is the count on develop 4667bd7b from the same venv; the two extra passes are the new
cases above.

Checklist

  • Updated CHANGELOG.md (if user-facing)
  • README / docs updated (not needed: no route, schema, or client-visible field changed)
  • Base branch is develop

Rebased onto develop@ded7addd on 2026-09-30. 1.0.2b4 and 1.0.2b5 both shipped since the previous base, and each release commit folded the whole ## [Unreleased] list into a version block, which emptied that section on develop — it is this branch's only conflict. The resolution keeps develop's CHANGELOG.md verbatim and re-inserts this PR's single bullet under a fresh ### 修复; nothing else changed.

Everything below was re-measured on develop@ded7addd. Backend gates were run by hand, as the equivalents of the CI steps (make lint / make typecheck / make test); make all was not used because this clone has no dashboard/node_modules.

  • Red, with this PR's fix reverted and only its tests: uv run pytest -q tests/unit/agents/test_agent_avatar.py -> 2 failed, 7 passed in 4.10s
  • Green as committed: the same command -> 9 passed in 1.51s
  • uv run ruff check src tests -> All checks passed!; uv run ruff format --check src tests -> 1111 files already formatted
  • uv run mypy src/octop -> Success: no issues found in 543 source files
  • Full suite uv run pytest -q -m "not live" -n 6 -> 3955 passed, 17 skipped, 1 failed in 214.63s, against 3953 passed, 17 skipped, 1 failed in 225.23s measured on the unmodified base commit in the same virtualenv. The single failure in both runs is tests/unit/test_plugin_manager.py::test_install_plugin_importing_legacy_harness_agent: legacy_imports.ensure_legacy_harness_agent_alias() deliberately leaves a real harness_agent distribution alone, and this local venv has an out-of-lock orcakit-harness-agent installed, so the alias assertion cannot hold here. It is pre-existing on the base, not a case this PR adds, and it is green in CI, which installs strictly from uv.lock (the Python 3.12 quality job is success on both this branch's pre- and post-rebase heads).
  • tests/live was not run; no model API key, PostgreSQL or Docker was needed.

Rebased onto develop@83b2ae2e7 (2026-10-03).

Four commits landed on develop since this branch's previous base (a02e3e18e): LDAP directory login (#1123), propagating exhausted model retries as failures (#1012), and two dashboard fixes (#1429, #1241) — each appended a bullet to ## [Unreleased], so CHANGELOG.md is this PR's only conflict. The resolution keeps develop's CHANGELOG.md verbatim and re-inserts this PR's single line into the existing ### 修复 list: git diff origin/develop 15cfc8d4b -- CHANGELOG.md is +1/-0, and git diff 905fce5687 15cfc8d4b2 over every non-CHANGELOG file of the PR is empty, i.e. the code you already reviewed is byte-identical.

Re-measured on the new base with the same commands and the same virtualenv as the Test plan above:

  • ruff check src tests → All checks passed!
  • ruff format --check src tests → 1133 files already formatted
  • mypy src/octop → Success: no issues found in 551 source files
  • this PR's own tests: pytest -q tests/unit/agents/test_agent_avatar.py → 9 passed in 1.75s
  • full suite pytest -q -m "not live" -n 3 → 1 failed, 4134 passed, 17 skipped, 13 warnings in 425.21s (0:07:05) (three branches measured concurrently, so the wall time is inflated), against 4132 passed, 17 skipped, 1 failed in 218.55s measured on the unmodified base 83b2ae2e7 in this same virtualenv: +2 cases against the base, which are exactly this PR's. The single failure in both runs is the pre-existing local artifact tests/unit/test_plugin_manager.py::test_install_plugin_importing_legacy_harness_agent — this venv has an out-of-lock orcakit-harness-agent distribution installed, and that test deliberately leaves a real harness_agent package alone, so its alias assertion cannot hold here. It is red on the untouched base too and green in CI, which installs strictly from uv.lock.

2026-10-06 — CHANGELOG.md re-aligned with develop. A rebase of this branch had re-introduced other PRs' release-note bullets into the released ## [1.0.2b6] block, so this PR's diff carried those foreign lines instead of only its own. CHANGELOG.md is now develop's file verbatim plus this PR's single bullet under ## [Unreleased] → ### 修复:

  • git diff --numstat 75c274aef 03d262bb7 -- CHANGELOG.md → 1\t0\tCHANGELOG.md
  • git diff --name-only c09c8974c 03d262bb7 → CHANGELOG.md only, so every code and test file is byte-identical to the head that Python 3.12 and Windows / Python 3.12 both reported success on;
    the commands and counts under Test plan therefore still describe this diff.
  • CHANGELOG.md at 75c274aef is identical to today's develop tip, so this does not need a fresh rebase.

Merged develop@97d6e7ff4 instead of rebasing (2026-10-12); head 03d262bb7 -> 7cf35467d.

This thread has a human review comment, so it is not eligible for a force-push: the branch was
moved forward with an ordinary git merge origin/develop and a plain (fast-forward) push, no
--force/--force-with-lease involved. CHANGELOG.md was the only conflict, resolved the same
way as the rebase lanes — develop's file verbatim plus this PR's own bullet under ### 修复,
+1/-0 against the new base; the code patch is byte-identical to the pre-merge one
(avatar.py +4, test_agent_avatar.py +28, hunk headers aside). Backend gates re-run on the
merged head (make all still not applicable — no dashboard/node_modules):

  • targeted tests for this branch: 10 passed in 42.95s
  • full suite: 4629 passed, 26 skipped, 17 warnings in 439.76s, against develop@97d6e7ff4 measured in the same venv: 4627 passed, 26 skipped, 0 failed (the +2 are this PR's two new cases)
  • ruff check src tests -> All checks passed!; ruff format --check src tests -> 1179 files already formatted; mypy src/octop -> Success: no issues found in 570 source files

Folded develop in with a merge commit (this PR has a human comment, so no history rewrite was used) develop@87a8d52c2 (2026-10-14); branch tip 7cf35467d -> 47fa90fc8.

CHANGELOG.md is again the only conflict. develop's file is kept verbatim and this PR's own
bullet is re-inserted into the current ## [Unreleased] list, which is now flat (no ###
subsections), so the diff against the new base is 1 added / 0 deleted there and the
2 code/test file(s) are byte-identical before and after (hunk headers aside) — checked
mechanically, not by eye. Gates re-run by hand on the new head (make all still does not apply:
no dashboard/node_modules in this clone; the venv belongs to this worktree, so
/Users/macos/Documents/Qoder/octop-pr-mining/repos/wt-r-1297/src/octop/__init__.py is this branch's own code):

  • RC ruff=0 format=0 mypy=0 targeted=0 full=0
  • targeted tests for this branch: 10 passed in 2.65s
  • full suite: 4681 passed, 26 skipped, 41 warnings in 237.43s (0:03:57), against develop@87a8d52c2 measured in the same environment today: 4679 passed, 26 skipped, 0 failed
  • All checks passed!; 1185 files already formatted; Success: no issues found in 571 source files

Merged develop@99e479a6b (2026-10-16) into this branch; head a930a67ed -> 543a181e3.

This branch keeps its history: a reviewer asked for a CHANGELOG re-join on 2026-09-30 and there is prior discussion on it, so the branch is fast-forwarded rather than rewritten. CHANGELOG.md is the only thing that had to be re-joined — this PR's bullet is moved back into the current ## [Unreleased] → ### 修复 list, develop's lines are kept verbatim, and the 2 code/test file(s) are byte-identical (checked on the patch hash, not by eye). Gates re-run by hand on the new head (make all still does not apply — no dashboard/node_modules in this clone; the venv belongs to this worktree, so the checkout under test is this branch's own code):

  • RC ruff=0 format=0 mypy=0 targeted=0 full=0
  • targeted tests for this branch: 10 passed in 8.53s
  • full suite: 4782 passed, 32 skipped, 41 warnings in 411.54s (0:06:51), against develop@99e479a6b measured today in its own per-worktree venv on the same machine: 4780 passed, 32 skipped, 0 failed in 495.68s
  • All checks passed!; 1203 files already formatted; Success: no issues found in 581 source files

@lihongyuan99
lihongyuan99 force-pushed the fix/avatar-delete-unsupported branch from 343ec62 to 692f1ef Compare September 29, 2026 23:29
@jubaoliang

Copy link
Copy Markdown
Collaborator

分诊:问题真实(BackendOperationNotSupportedError 是 RuntimeError,会 500)。映射到 WORKSPACE_OP_UNSUPPORTED 正确。

请 rebase CHANGELOG 到当前 Unreleased 后再合。CI 已绿。

@lihongyuan99
lihongyuan99 force-pushed the fix/avatar-delete-unsupported branch from 692f1ef to 905fce5 Compare September 30, 2026 15:51
@lihongyuan99

Copy link
Copy Markdown
Contributor Author

Done — 905fce56 is rebased onto develop@ded7addd, so the entry now sits inside the current ## [Unreleased] → ### 修复 list.

Checked against today's develop@a02e3e18: git merge-tree --write-tree origin/develop 905fce56 resolves with no conflict, and the merged ## [Unreleased] keeps upstream's three entries plus this one line.

CI on 905fce56: Python 3.12 success, Windows / Python 3.12 success, Live tests skipped (fork, no secrets).

@lihongyuan99
lihongyuan99 force-pushed the fix/avatar-delete-unsupported branch 3 times, most recently from 5583835 to c09c897 Compare October 5, 2026 12:49
BackendOperationNotSupportedError is a RuntimeError, so the harness signal for a
backend without a delete primitive escaped delete_workspace_avatar() and turned
POST/DELETE /api/agents/{id}/avatar into an unhandled 500 on OpenSandbox-backed
agents. Map it to the WORKSPACE_OP_UNSUPPORTED code the workspace router already
uses for the same condition.
@lihongyuan99
lihongyuan99 force-pushed the fix/avatar-delete-unsupported branch from c09c897 to 03d262b Compare October 5, 2026 23:14

This branch has not been deployed

No deployments
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.

2 participants