Skip to content

fix(jsonl): map LifecycleError to invalid_request, not internal_error - #723

Open
Yurii214 wants to merge 1 commit into
vouchdev:testfrom
Yurii214:fix/jsonl-lifecycle-error-taxonomy
Open

fix(jsonl): map LifecycleError to invalid_request, not internal_error#723
Yurii214 wants to merge 1 commit into
vouchdev:testfrom
Yurii214:fix/jsonl-lifecycle-error-taxonomy

Conversation

@Yurii214

Copy link
Copy Markdown
Contributor

summary

the jsonl/http dispatcher classifies a LifecycleError as internal_error (a server-fault code) instead of invalid_request (a caller-error code). lifecycle.LifecycleError is a RuntimeError, and the dispatcher's clean-caller-error catch only covers (ValueError, ProposalError, ArtifactNotFoundError) — so LifecycleError falls through to the bare except Exception and is logged + returned as internal_error.

concretely, kb.set_goal_status with a bad status:

  • lifecycle.set_goal_status validates the status before the goal lookup and raises LifecycleError("unknown goal status …").
  • the dispatcher maps that to {"code": "internal_error"}, which clients treat as retryable/alertable — for pure bad input.

the same method is self-inconsistent: an unknown goal id raises ArtifactNotFoundError, which is in the tuple → correctly invalid_request. so a typo'd id is a clean caller error but a typo'd status is a "server fault".

why it's the same class as ProposalError

ProposalError is proposals.py's domain error (also a RuntimeError) and is already mapped to invalid_request. LifecycleError is lifecycle.py's exact sibling and was simply missing from the tuple. all four LifecycleError raise sites — unknown/no-op goal status, self-supersede, self-contradict — are caller conditions, never internal faults, so mapping the whole class is correct.

the other two surfaces already agree: mcp kb_set_goal_status wraps except (LifecycleError, ArtifactNotFoundError): raise ValueError(...), and the cli lists LifecycleError in _cli_errors. tests/test_goals.py states the contract directly — "a host sees ValueError, never an internal exception". jsonl/http were the outlier.

fix

add life.LifecycleError to the dispatcher's invalid_request tuple (life is already imported). one-token change plus a why-comment; no behaviour change for any path that was already correct.

test

test_jsonl_set_goal_status_bad_status_is_invalid_request sends kb.set_goal_status with status="bogus" and asserts error.code == "invalid_request". it fails on the current code (internal_error) and passes with the fix. (status is validated before the goal lookup, so no goal need exist.)

validation

ruff check src tests, mypy src (123 files, no issues), and full pytest tests/ --ignore=tests/embeddings (2991 passed) all green.

the jsonl/http dispatcher's clean-caller-error catch omitted LifecycleError
(lifecycle.py's domain error — a RuntimeError sibling of the already-handled
ProposalError), so a caller mistake returned the server-fault code
internal_error instead of invalid_request, which clients treat as retryable:
a typo'd goal status on kb.set_goal_status, or a self-supersede/-contradict.
mcp already maps LifecycleError to ValueError (server.py) and the cli catches
it; this brings jsonl/http in line. an unknown goal id already returns
invalid_request via ArtifactNotFoundError, so the same method was
self-inconsistent — a bad id was a caller error but a bad status a server fault.
@Yurii214
Yurii214 requested a review from plind-junior as a code owner July 31, 2026 16:57
@github-actions github-actions Bot added mcp mcp, jsonl, and http surfaces tests tests and fixtures size: XS less than 50 changed non-doc lines labels Jul 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

mcp mcp, jsonl, and http surfaces size: XS less than 50 changed non-doc lines tests tests and fixtures

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant