Skip to content

GET /api/v1/packages and /meta/package/<showcase> answer 500 "Converting circular structure to JSON" — the registry stores the live defineStack manifest, plugin instances included #14309

Description

@hotlong

What happens

On a stock showcase boot (main @ a39b02a, objectui pin 67dadd602a3a, objectstack dev --seed-admin), signed in as the seeded admin:

GET /api/v1/packages                          -> 500 INTERNAL_ERROR
GET /api/v1/packages/com.example.showcase     -> 500 INTERNAL_ERROR
GET /api/v1/meta/package/com.example.showcase -> 500 {"error":"Internal server error"}
GET /api/v1/meta/package/com.objectstack.setup -> 200

Error body of the first two:

Converting circular structure to JSON
    --> starting at object with constructor '_ObjectQL'
    |     property 'actionActivation' -> object with constructor 'ActionActivationProjection'
    |     property 'store' -> object with constructor 'ObjectStoreActionActivationStore'
    --- property 'engine' closes the circle

Studio calls GET /api/v1/packages three times on every open and gets 500 each time (package switcher / publish surfaces read it). The 500 is not logged server-side (filed separately).

Why

  • ObjectQL.registerApp(manifest) hands the app's defineStack(...) object to SchemaRegistry.installPackage(manifest) (packages/objectql/src/engine.ts ~4795), and installPackage stores that object verbatim as pkg.manifest (packages/objectql/src/registry.ts ~3575-3620).
  • The showcase manifest carries live runtime plugin instances (plugins: [new ConnectorRestPlugin(), new ConnectorMcpPlugin(), MarketplaceProxyPlugin, RuntimeConfigPlugin, ...], examples/app-showcase/objectstack.config.ts ~134-162). After init those instances hold the engine.
  • Since feat(actions): durable packaged-action disable — same activation ledger, dispatch-time consult #12348 (2026-08-25) the engine carries actionActivation -> store -> engine, a reference cycle, so JSON.stringify of anything that reaches the engine throws. Before that PR the same response would have serialized the entire engine graph into the payload instead of failing, which is why this only surfaces now.
  • GET /packages spreads every registry item into the response as-is (packages/rest/src/package-routes.ts ~662-700, { ...item, source: 'registry' }), so one unserializable package item fails the whole list. The Setup package has no plugin instances, hence 200.

Suggested fix

  1. Store a serializable manifest projection at install time: installPackage should keep the spec-shaped manifest (id, name, version, namespace, type, scope, description, dependencies, declared metadata) and drop runtime-only members such as plugin instances. The kernel keeps the live object; the registry item is a record, not the runtime.
  2. GET /packages / GET /packages/:id should project explicit fields rather than spreading the item, so a future non-serializable member degrades to a missing field instead of a 500.
  3. Pin it: a test that registers a package whose manifest carries an object with a reference cycle and asserts JSON.stringify(registry.getPackage(id)) succeeds, plus a route test on GET /packages over the showcase-shaped stack.

Repro

pnpm dev -- --fresh -p 3911     # showcase
curl -s -c c.txt -H 'Content-Type: application/json' -d '{"email":"admin@objectos.ai","password":"admin123"}' http://localhost:3911/api/v1/auth/sign-in/email
curl -s -b c.txt http://localhost:3911/api/v1/packages | head -c 400

Found during the objectui pin-bump dogfood (PR #14295); not caused by that bump.

Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:enginepriority:p1High: required for production / M2

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions