Skip to content

seren-skills bundle download returns ApiResultResponse wrapper instead of SkillBundle; diagnostic eats the inner error #1925

@taariq

Description

@taariq

Symptom

Browser console (Seren Desktop, latest main) prints repeatedly during normal idle:

Unexpected seren-skills bundle response for curve-gauge-yield-trader: {data} -> {status,body,response_bytes,execution_time_ms,cost,asset_symbol} -> {error}

The skill curve-gauge-yield-trader is locally installed under ~/.config/seren/skills/curve-gauge-yield-trader. The skill works at runtime — what fails is the periodic update-check / sync-state probe that downloads the published bundle to diff revisions.

What the envelope trail tells us

describeBundleEnvelope in src/services/skills.ts:206-245 walks the response and prints the keys at each depth. The trail is:

Depth Keys observed Notes
0 {data} Outer destructure from the generated downloadSkill() SDK call
1 {status, body, response_bytes, execution_time_ms, cost, asset_symbol} This is the ApiResultResponse schema from openapi/openapi-seren-private-models.json:147 — the gateway's call_publisher / proxy wire format
2 {error} Inside body; walker stops here because error is an object, not a string

So the seren-skills download endpoint returned a 200 OK whose body is wrapped in the publisher-call proxy envelope, and the upstream actually failed (the body.error is populated). The client correctly rejects it — that part is working. The two issues:

  1. Server / proxy contract violation. Per openapi/openapi-seren-skills.json, GET /skills/{slug}/download returns SkillBundle directly ({skill, version, skill_md, manifest, content_hash, files?}) on 200, and the proxy is supposed to pass through HTTP error statuses on failure. Today it appears to wrap the upstream failure as a 200 with ApiResultResponse instead. We need to confirm where the wrap happens and decide whether to fix the server or formalize the wrap in the spec.
  2. Client diagnostic is uninformative for structured errors. The walker only surfaces a server error string when obj.error is a string (or obj.message is a string adjacent to error/code/status). When body.error is a structured object, the walker prints the layer keys and stops, so the actual upstream error message is never logged. Operators can't root-cause without re-running with the network tab open.

Code-path audit (client)

  1. src/services/skills.ts:1360fetchRemoteSkillRevision(skill.upstreamSourceUrl) is called from the sync-state pipeline for every installed skill on each invocation.
  2. src/services/skills.ts:520-526fetchRemoteSkillRevision resolves the slug from the seren-skills source URL and calls downloadSkillBundle(slug).
  3. src/services/skills.ts:312-328downloadSkillBundle calls the generated downloadSkill({ path: { slug }, throwOnError: false }) SDK, which hits GET /publishers/seren-skills/skills/{slug}/download via the hey-api client (base URL configured at src/api/generated/seren-skills/client.gen.ts:18).
  4. src/services/skills.ts:293-310normalizeSkillBundle walks the response envelope via findInResponseEnvelopes; in the failure case the inner candidate has {status, body, response_bytes, ...} rather than the expected {skill_md, content_hash, skill} and returns null.
  5. src/services/skills.ts:323-325 — throws the message reported in the console, with the (currently truncated) envelope trail.

The customFetch wrapper at src/api/client-config.ts:12-55 does not intervene because the gateway returned 200 OK; it never sees a 4xx/5xx to trigger error reporting.

Code-path hypothesis (server)

The ApiResultResponse schema (status, body, response_bytes, execution_time_ms, cost, asset_symbol, payment_source) is defined in openapi/openapi-seren-private-models.json and openapi/openapi-seren-models.json as the wire format for chat-completion and publisher-proxy responses. Two plausible causes for it leaking into the seren-skills download path:

  1. The seren-skills publisher itself wraps non-2xx upstream responses in ApiResultResponse for telemetry/billing and returns 200 to the proxy. The proxy then passes that JSON through unmodified.
  2. The proxy (/publishers/{slug}/{path} in openapi/openapi.json) converts errors into the same wrap shape, even though the proxy's documented 200 schema is "depends on publisher".

Either way, the client's contract with /publishers/seren-skills/skills/{slug}/download is SkillBundle | 4xx/5xx, not 200 + ApiResultResponse. Someone needs to hit the endpoint directly with the desktop session's auth and capture the raw response to confirm which layer is doing the wrap and what body.error actually contains for this slug. Plausible upstream causes for curve-gauge-yield-trader specifically: the slug isn't published in the seren-skills catalog (user installed it from elsewhere), or the published version was deleted, or the caller doesn't have read access.

Solution

Part 1: client diagnostic (already implemented, awaiting PR)

Worktree: .worktrees/fix-bundle-envelope-error-object, branch fix/bundle-envelope-error-object.

Change in src/services/skills.ts:206-245 (describeBundleEnvelope): when obj.error is a structured object, pull message / detail / error / code (first string wins) from inside it and surface as (server error: ...) — same shape as the existing string-valued branch, just one level deeper. Test added at tests/unit/skills-index-api.test.ts (sibling to the existing "surfaces server error envelopes" case) using the exact production envelope shape. All 12 tests pass.

This does not fix the underlying server behavior — it just guarantees the next reproduction surfaces the actual upstream error message in the console line so we can debug it without packet capture.

Part 2: server contract (follow-up, needs investigation)

Pick one and stick with it:

  • (A) The seren-skills /skills/{slug}/download endpoint should return native HTTP errors (404 for missing slug, 403 for access denied, 5xx for upstream failure) and let the proxy pass them through. The current ApiResultResponse wrap on failure is incidental and should be removed.
  • (B) If ApiResultResponse wrapping is intentional (billing, telemetry, retry budget), update openapi/openapi-seren-skills.json to document a oneOf SkillBundle | ApiResultResponse 200 response and have the client unwrap explicitly via a typed branch.

Either is reasonable; (A) is less work on the client side. We need someone to reproduce against the live endpoint with curve-gauge-yield-trader to capture body.error and decide.

Reproduction

  1. Open Seren Desktop.
  2. Confirm curve-gauge-yield-trader is installed locally under ~/.config/seren/skills/.
  3. Trigger any UI action that runs the skills sync-state probe (or wait for the periodic check).
  4. Open DevTools console — the warning appears verbatim.

Acceptance criteria

  • After the client-side fix lands, the console line for this case includes (server error: <message from body.error>) instead of stopping at {error}.
  • After the server-side decision lands, either (a) the call returns a normal 4xx/5xx and the client's existing error path handles it, or (b) the OpenAPI spec and normalizeSkillBundle both account for the ApiResultResponse wrap explicitly.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions