Skip to content

refactor(server): the server answers /api/copilotkit/info itself; @copilotkit/runtime leaves - #230

Merged
yulmucoffee merged 4 commits into
mainfrom
laf/own-info-route
Oct 13, 2026
Merged

yulmucoffee merged 4 commits into
mainfrom
laf/own-info-route

Conversation

@yulmucoffee

Copy link
Copy Markdown
Collaborator

What this changes

server/ no longer depends on @copilotkit/runtime. The one thing it did here — answer GET /api/copilotkit/info for the app's CopilotKit client, with every other route it knew already refused in its hook — is a Hono route of our own in server/src/copilot.ts (mountCopilotRuntime, same name, same signature, same mount in main.ts/app.ts, same requireUser in front). The answer is what runtime 1.67.1 gave, byte for byte, from the same per-request roster (createRequestAgents), so @ag-ui/client, the forwardedProps middleware, the tools fingerprint and the agents map are untouched. @copilotkit/react-core (app) and @copilotkit/aimock (dev) are untouched.

Why it is worth it: the runtime was a router, a runner, a GraphQL server, five model SDKs and a channels package loaded at every boot to answer one question.

measured on this Mac (Bun 1.3.14), 5 runs each main ecd5eb23 this branch
cold start: spawn → first 200 on /health, median (range) 585 ms (561–887) 348 ms (320–379)
resident memory 5 s after boot (ps rss), median (range) 311.3 MiB (309.2–319.0) 251.2 MiB (250.5–254.8)
memory footprint after 10 min idle (top MEM, compressed pages included), five of each idling side by side, median (range) 242 M (239–243) 176 M (172–178)
package names in bun.lock (against main 12c1c37e) 986 815 (171 gone, 0 added)
lock entries / name@version pairs 1,298 / 1,157 1,045 / 942

The time and memory rows were measured before the rebase, branch point ecd5eb23 against the branch; nothing that landed on main since touches what the server loads at boot. On the idle row: ps rss after ten minutes is not a measure on macOS — idle pages are compressed and leave RSS, by an amount that depended on what else the machine was doing (main's five read 95 MiB and this branch's 162–170 MiB in the same minute, with 190 M and 50 M compressed). The footprint, which counts compressed pages, is the row above.

bun.lock: the diff is deletions, plus the MCP SDK's express 5 and @hono/node-server 2.1 moving up from under @modelcontextprotocol/sdk/… now that nothing pins older ones beside them. No name@version appears that was not in the lock before (compared set against set).

Before / after, request by request

A script booted the server against a database of its own and recorded status, every header and the body for 59 requests under /api/copilotkit — each method × path copilot.test.ts exercises, plus OPTIONS, HEAD, TRACE, trailing slash, deeper paths, the bare base path, the single-route envelope, a malformed body, a foreign Origin, and three neighbours outside the base — on main and on this branch, same database state (one live Bot, one deleted).

  • No session: 59 of 59 byte-identical. The guard is the same middleware and answers first.
  • Signed in: 27 identical but for one header — the runtime added Access-Control-Allow-Origin: * to every response and this route does not. Among them the four GET /info answers (557 bytes, equal byte for byte, Content-Type: application/json), the 19 closed doors of the test's list and a malformed-body run (404 {"code":"laf:not_found"}, application/json;charset=utf-8), and the three neighbours.
  • 32 differ, none on a request the app makes. The app's client asks exactly GET ${runtimeUrl}/info (and, only if that fails, POST to the base, which was and is a 404); nothing else in app/, desktop/, agent-bot/ or scripts/ names the path.
what count main this branch
other spellings of info: /info/, /x/info, //info 3 200, the roster — the runtime's router read a route off a path's last segments 404 laf:not_found
paths the runtime did not know (bare base, /nothing, /INFO, the envelope…) 14 404 {"error":"Not found"} 404 {"code":"laf:not_found"}, the server's own
another method on /info (POST, PUT, PATCH, DELETE, HEAD, TRACE) 7 405, Allow: GET, {"error":"Method not allowed"} 405, Allow: GET, no body
OPTIONS anywhere under the base 5 204 with Access-Control-Allow-Methods/-Headers: * 405 on /info, 404 elsewhere
wrong method on a route the runtime knew and we closed 3 405 with that route's Allow 404 laf:not_found

Decided by reading, not kept by accident:

  • The wildcard CORS header and the 204 preflight are dropped. The server sets no CORS header anywhere else; the app reaches the API same-origin (the Vite proxy in dev, one origin in a deployment), and asks /info with credentials: "include", which a browser refuses to pair with * — so no page on another origin could have been reading this with a session.
  • The bodies that were the vendor's sentences are not carried over. error-codes.test.ts ("no refusal anywhere in the server is a sentence") failed on them the moment they were ours. Status and Allow are kept.
  • A roster that cannot be read (the database away) was caught inside the runtime: nothing logged, and {"error":"Failed to retrieve runtime information","message":<the error's own message>} sent to the page. It is thrown to app.onError now: route_failed in the log, 500 {"code":"laf:internal"} to the page. Still a 500 to the client, which reads only the status.

Where it runs

  • New state that outlives a request? None.
  • New listener, port, or schedule? None. One process loads less at boot (table above).
  • Anything the deployment cannot honour? No control added.

Boundary and audit

  • Every acting call still goes through the gateway: untouched.
  • New refusals and new failures each write a row: none new; a roster failure on /info is now logged (route_failed) where it was silent.
  • Nothing new is trusted from the client that the server can resolve itself: the roster is still resolved from the session, per request.
  • Nothing new lets an action past a person.
  • A Bot cannot write the rule that decides whether it gets asked about: untouched.
  • Nothing new records a value somebody typed.

Korean

  • No user-facing string added or changed.
  • No string read through a variable.
  • The server sends facts: two vendor sentences that used to reach the client under this path no longer do.

The gate

  • All four pass on 2bc582b0 (rebased on main 12c1c37e), after a clean bun install: typecheck exit 0 · biome lint no errors (2 warnings and 1 info, all three in files this does not touch) · format:check clean · test:ci (2 workers) 9,812 tests: server 4,724 · app 2,663 · agent-computer 650 · root 1,775.
  • Floor: server 4,666 → 4,683, the seventeen tests added to server/tests/copilot.test.ts (42 → 59 in that file), in its own commit.
  • tests/dependencies.test.ts loses the runtime's line and the seven licence exceptions only its tree needed (@ag-ui/*@0.0.54 ×4, @ag-ui/mcp-apps-middleware, @ag-ui/mcp-middleware, limiter) — the test named each of them as stale once the package was gone.
  • Every test added was seen failing with the line it guards reverted (below).

The existing tests

The three cases of "the runtime's doors" keep their assertions: info is 200 with the roster and mode: "sse" and contacts no Bot; the nineteen closed routes are 404 {"code":"laf:not_found"} and contact no Bot; POST /api/copilotkit is 404 and POST /info is 405. Two titles changed a word ("the runtime knows" → "a runtime knows"), and the file no longer imports telemetry-off first — that was for the runtime it built.

The seventeen, and how each was seen red

Each line below is one edit to server/src/copilot.ts (the last one to app.ts), bun test tests/copilot.test.ts, restore.

reverted / broken tests that went red
telemetryDisabled: true → false; className spelled as a literal; a threadEndpoints value flipped; the version constant changed (four separate runs) "info's answer is the one the app's client was written against, whole" (each time)
the roster kept from the first ask "the roster is read again on every ask…"
a roster failure caught and worded in the route "a roster that cannot be read is the server's own 500, and its reason stays on the server"
the method check removed the five "info is a GET and nothing else: %s" · "a HEAD on info is refused as it was…" · "nothing under the endpoint invites another origin…"
Allow dropped; a sentence put back as the 405's body (two runs) the five "info is a GET and nothing else: %s" (and the HEAD case for Allow)
the route matched by a path's last segments again all five "info is one path: %s is not it"
Access-Control-Allow-Origin: * put back "nothing under the endpoint invites another origin to read it"
the catch-all removed "every other route a runtime knows…" · "a path that is nothing a runtime knows…"
the base path dropped from the handler "mounted in the app › a signed-in person is answered the roster at the path the app asks" (and twelve others)
app.use("/api/copilotkit/*", requireUser) removed "mounted in the app › nobody signed in is refused before the roster is read"

Proof

Measured, on the server at :3461 and the app at :3462 against a database made for this and dropped after, headless Chrome through Playwright:

  • The first screen makes a Bot, the conversation opens, and the page's one request under the path is GET /api/copilotkit/info → 200, 401 bytes, the same body main's server gave for that roster.
  • A streamed reply renders past its first chunk: asked for the four seasons in two sentences each; the page's text was sampled as it changed and grew through fifteen distinct lengths between 4.9 s and 7.4 s after sending, ending at the full eight sentences (screenshot read).
  • A card draws: asked for a checklist card; the showChecklist card drew in the transcript and in 홈's 만든 것, three items.
  • A page opens: asked for example.com's title; the browser step line drew ("example.com · Example Domain", 끝남) and the answer followed. This did not ask first on this stack.
  • An approval card appears and answering it continues the run: asked to be asked; the card drew with 답을 기다려요 and the Bot showed 일하는 중; pressing 승인 turned the card to 승인함 and the run went on to its closing sentence.

Those four turns ran on the first cut of the route; the cut that followed changed only the 405's body and the failure path. On the final commit the conversation was reloaded in the browser (/info 200, no Access-Control-Allow-Origin) and the 59-request dump above was taken.

Model spend, read from the trail of that database: $0.0046 (11 Bot calls, and the decision model's calls at each boot of the server).

Left, on purpose

  • server/src/index.ts and server/scripts/test-preload.ts still load eventsource first, and eventsource is still a dependency. Their stated reason was a CommonJS module in the runtime's tree; bundling server/src/main.ts on this branch (bun build --target=bun) finds no eventsource in the graph at all (50 packages, eventsource-parser the only near name). Whether the preload can go needs the concurrent-boot run its comment describes, on Linux — not done here.
  • server/src/telemetry-off.ts sets COPILOTKIT_TELEMETRY_DISABLED, which nothing in the server reads any more; it, its import in main.ts, its entry in config-discipline.test.ts and the row in docs/configuration.md are outside this change's files.
  • The root package.json override of uuid was recorded (docs/laf/deploying.md) as being for the runtime's ^10; not re-examined.
  • /info still says threadEndpoints.list/inspect: true, as the runtime did, though those routes are closed. Kept for the byte; the client makes no request from it.
  • /info itself goes when the app's side stops asking for it.

🤖 Generated with Claude Code

@yulmucoffee

Copy link
Copy Markdown
Collaborator Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 2bc582b. Configure here.

@codesolarlght codesolarlght Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔆 CodeSolar review ✅ Looks good

Refactors the server to answer /api/copilotkit/info with its own Hono route instead of @copilotkit/runtime, removing the runtime dependency. The new route returns a byte-identical response for the app's client, closes all other paths with 404, and refuses non-GET methods on /info with 405. Tests cover the full request surface including the real handler behind the real auth guard.

✅ No issues worth flagging.


solar-pro4 (openrouter) · 6 files · +352/-582 · 278.9s · CodeSolar

Comment /codesolar on this PR to re-run the review.

yulmucoffee and others added 4 commits October 13, 2026 19:28
The CopilotKit runtime was mounted to answer one question, GET
/api/copilotkit/info, for the app's client; every other route it served
was already refused in its hook. The route is a Hono route in copilot.ts
now and answers what runtime 1.67.1 answered, byte for byte, from the
same per-request roster, behind the same guard.

What differs is on requests the app never makes:
- info is the exact path. The runtime's router read a route off a
  path's last segments, so /x/info and /info/ were info too.
- No response carries Access-Control-Allow-Origin: * and a preflight is
  not answered 204. The app is same-origin and asks with its cookie,
  which a wildcard cannot be used with.
- Another method on info is still 405 with Allow: GET, without the
  runtime's sentence for a body; a path that is not info is the
  server's own 404 whatever its method.
- A roster that cannot be read is thrown to the app's error handler,
  which logs it and answers its own 500. The runtime logged nothing
  and put the error's message in the body.

Co-authored-by: LAF Agent <274876363+laf-agent@users.noreply.github.com>
Nothing imports it since the info route became the server's own. 171
package names leave bun.lock (986 to 815) and no name@version is added:
the MCP SDK's express and @hono/node-server, nested while the runtime
pinned older ones, move up. The dependency list loses its line and the
seven licence exceptions only its tree needed.

Co-authored-by: LAF Agent <274876363+laf-agent@users.noreply.github.com>
… 4683)

Co-authored-by: LAF Agent <274876363+laf-agent@users.noreply.github.com>
…s seventeen

Co-authored-by: LAF Agent <274876363+laf-agent@users.noreply.github.com>

@codesolarlght codesolarlght Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔆 CodeSolar review ✅ Looks good

🔄 2nd review of this pull request · 11 new commit(s) since the last one

The server now answers GET /api/copilotkit/info with its own Hono route instead of @copilotkit/runtime. The route returns the per-request agent roster byte-for-byte as the runtime did, and every other path under the base returns 404. Tests cover the full response shape, per-request roster resolution, method/spelling restrictions, absence of CORS headers, and the real handler behind the real auth guard. The mountCopilotRuntime signature changed: stallGuard is now required and timeZone has no default.

✅ No issues worth flagging.


solar-pro4 (openrouter) · 6 files · +352/-582 · 299.8s · CodeSolar

Comment /codesolar on this PR to re-run the review.

@yulmucoffee
yulmucoffee merged commit 9ea8e6f into main Oct 13, 2026
5 checks passed
@yulmucoffee
yulmucoffee deleted the laf/own-info-route branch October 13, 2026 10:36
yulmucoffee added a commit that referenced this pull request Oct 13, 2026
… the variable

It set COPILOTKIT_TELEMETRY_DISABLED as the first import of main.ts, for
@copilotkit/runtime, which read it and left the server in #230.

Checked before removing: the bundles of the server's three entries
(main.ts, converter.ts, scripts/migrate.ts) name the variable once, in the
line that wrote it; the 177 packages the server image installs name
neither it nor DO_NOT_TRACK outside bun's own type declarations. In the
whole workspace the one package that reads it is @copilotkit/shared, which
is the app's and never loaded by the server. DO_NOT_TRACK is still set by
the server for the two children it starts (converter-process.ts,
workbench/runner.ts); those are untouched.

With it go its import, its entry in config-discipline.test.ts and its row
in docs/configuration.md.

Co-authored-by: LAF Agent <274876363+laf-agent@users.noreply.github.com>
yulmucoffee added a commit that referenced this pull request Oct 13, 2026
…it has now

undici: nothing in bun.lock asks for it since @copilotkit/runtime left
(#230) — `bun why undici` finds no package — so the override named
nothing. tests/workspace.test.ts keeps the lockfile's half of its check: a
5.x that comes back still fails there.

uuid: recorded as being for the runtime's ^10. Three packages still ask
for it (@ag-ui/client and @copilotkit/shared ^11.1.0, mermaid ^11.1.0 ||
…), each in a range that admits 11.1.0, the version GHSA-w5hq-g745-h8pq is
about, so the override stays as the floor and docs/laf/deploying.md says
so. The lockfile resolves to one uuid@11.1.1 with or without it (tried in
a copy of the manifests, from the existing lock and from none).

The comment on qs is corrected the same way: it is under the MCP SDK's
Express alone now.

Co-authored-by: LAF Agent <274876363+laf-agent@users.noreply.github.com>
yulmucoffee added a commit that referenced this pull request Oct 13, 2026
… now

Since #230 the route under /api/copilotkit is the server's own. Four
comments in copilot.ts, two in app.ts and two in main.ts still described
CopilotKit's runtime as the thing mounted, served or guarded. Corrected to
what is true; where a comment records what went wrong with the runtime
(the unguarded mount, the eventsource import that broke the tests), the
history is kept and dated.

No code changes.

Co-authored-by: LAF Agent <274876363+laf-agent@users.noreply.github.com>
yulmucoffee added a commit that referenced this pull request Oct 13, 2026
server/src/index.ts loaded `eventsource` before main.ts, and the test
suite preloaded it (bunfig.toml, server/scripts/test-preload.ts), to settle
a boot race: a CommonJS `require("eventsource")` inside
@copilotkit/runtime's tree against an ESM import of the same module. The
runtime left in #230.

The graph: following every static import, require and literal dynamic
import with Bun's resolver, through node_modules, from server/src/index.ts,
main.ts, converter.ts, server/scripts/*, scripts/*, evals/* and all 654
test files, reaches `eventsource` from none (a probe importing the SDK's
client/sse.js does reach it, so the walk can see it). The bundles of the
three server entries hold no eventsource module.

The experiment the old comment describes, in the image server/Dockerfile
builds (oven/bun:1.3.14, two CPUs), thirty cold boots at once, five
rounds, each boot counted at its `boot` log line:

  tree before #230, entry without the preload, from source   72 of 150 died (all that TypeError)
  main, with the preload, from source / as the bundle        0 of 150 / 0 of 150
  this branch, without it, from source / as the bundle       0 of 300 / 0 of 300

Cold start, one boot at a time, spawn to the boot line, median of 5 in
each of three containers: bundle 175 / 195 / 172 ms on main and 175 / 173 /
169 ms here; from source 293 / 295 / 292 ms and 294 / 289 / 324 ms.

`eventsource` stays in bun.lock as the MCP SDK's own dependency; it is
no longer one of ours, so its line leaves tests/dependencies.test.ts and
the Dependabot ignore that kept ours on the SDK's major leaves too.
index.ts keeps the history.

Co-authored-by: LAF Agent <274876363+laf-agent@users.noreply.github.com>
yulmucoffee added a commit that referenced this pull request Oct 13, 2026
…d a runtime on the server

`@copilotkit/runtime` left the server in #230 and `@copilotkit/react-core`
left the app in #234; these still said, in the present tense, that they were
there. What happened is kept as history, and what is said of today is true:

- CLAUDE.md: the Tech line names AG-UI alone, and the React Compiler section
  keeps the bug with CopilotKit's message array as a past one. Shorter than
  it was (25,865 → 25,856 characters).
- The app: the provider on the conversation, compose and signed-in routes is
  the tools' own; the registry, not CopilotKit, draws a renderer through a
  memo and hands a decision card no `respond`; the session watch wraps no
  runtime calls; the transcript is AG-UI's messages.
- The server: who is asking is resolved for the screen socket alone, and its
  refusals no longer call the request a CopilotKit run; the thread-id note is
  in the past tense; the log test's header names the AG-UI client.
- `shared/tools/standard-schema.ts` (comment only): the conversion to JSON
  Schema is the app's own `tool-schema.ts`. The tool list's bytes did not
  move (tool-list-bytes.test.tsx, 7 pass).
- `shared/whereabouts.ts` (comment only): the device rides with each message.
- docs/laf/design-tokens.md §9: the `.dark` block that beats inheritance on a
  nested element is `prompt-area/tailwind.css`'s. The vendored
  `app/src/shadcn-tailwind.css` has none. Measured again in a browser: under
  a light root, a `.dark` element has `--sand-bg-elevated: #1b1c20` and
  `--popover: oklch(0.205 0 0)`.
- docs/laf/deploying.md: only `@ag-ui/client` still asks for a `uuid` range
  the override is a floor under; the other two are gone from the lock.
- docs/development.md: the smoke journey's first step, as it now asks.

Co-authored-by: LAF Agent <274876363+laf-agent@users.noreply.github.com>
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.

1 participant