You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
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
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.
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.
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.
What happens
On a stock showcase boot (
main@ a39b02a, objectui pin 67dadd602a3a,objectstack dev --seed-admin), signed in as the seeded admin:Error body of the first two:
Studio calls
GET /api/v1/packagesthree 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'sdefineStack(...)object toSchemaRegistry.installPackage(manifest)(packages/objectql/src/engine.ts~4795), andinstallPackagestores that object verbatim aspkg.manifest(packages/objectql/src/registry.ts~3575-3620).plugins: [new ConnectorRestPlugin(), new ConnectorMcpPlugin(), MarketplaceProxyPlugin, RuntimeConfigPlugin, ...],examples/app-showcase/objectstack.config.ts~134-162). After init those instances hold the engine.actionActivation -> store -> engine, a reference cycle, soJSON.stringifyof 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 /packagesspreads 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
installPackageshould 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.GET /packages/GET /packages/:idshould project explicit fields rather than spreading the item, so a future non-serializable member degrades to a missing field instead of a 500.JSON.stringify(registry.getPackage(id))succeeds, plus a route test onGET /packagesover the showcase-shaped stack.Repro
Found during the objectui pin-bump dogfood (PR #14295); not caused by that bump.
Generated by Claude Code