Commit Graph

3049 Commits

Author SHA1 Message Date
Lukas Moschitz 4f432848f5 docs(oss-904): correct the claims about thread requests and the error state
Five comments justified routing thread requests through the instrumented fetch
by saying it lets opening a view restore the status after an outage. It does
not: every binding withholds its thread requests until the status is already
connected, so while it is red nothing is sent. The justification is DETECTION
only, which is what the CopilotChat site already said correctly.

Also:

- Documents both meanings of the Error state and the invariant behind them —
  the status reports the last actual contact with the runtime — on the
  connection-status reference page, which described only the startup meaning.
- Renames RUNTIME_PROBE_TIMEOUT_MS to ɵRUNTIME_PROBE_TIMEOUT_MS. core/index.ts
  re-exports agent-registry wholesale, so a constant whose own doc says
  "exported for tests" was public API of @copilotkit/core.
- Guards the Inspector's read of ɵruntimeFetch the way it guards its four other
  internal core accessors. A newer Inspector against an older pinned core was
  handing the thread store `undefined`, which breaks the Threads view outright
  rather than merely losing detection through it.
- Corrects the stop-request comment, which claimed to be the only runtime
  destination off the seam; the suggestion route's stateless path, the memory
  store and /inspector-metadata are too, just not by design.
- Corrects OSS-904-VERIFY.md, which said scenario 4 had real traffic to work
  with and left it off the not-covered list.
2026-08-26 13:25:13 +02:00
Lukas Moschitz dd1639726d test(core): close six mutations the suite let through
Each of these passed the whole core suite with the production code
deliberately broken; each new test was verified by re-applying the mutation.

- The watchdog becoming a repeating interval: nothing kept the clock running
  past the first fire, so a fresh probe every ten seconds for as long as one
  request hangs went unnoticed. Pinned by count and by getTimerCount.
- Deleting the "only probe while Connected" guard: a developer pressing Send
  three times at a red indicator bought three extra probes and three duplicate
  wiring errors.
- Rerouting the stop request onto the seam, which would make pressing Stop
  against a dead runtime turn the status red.
- Putting the agent-level auto-detect /info back on the global fetch. Both new
  suites pinned "rest", so the product default was never exercised.
- "A success on a non-critical request still counts", documented and asserted
  nowhere.
- isAbortError's string branch was unreachable from either call site, so it is
  removed rather than tested; suggestion-engine's own isAbortError already
  answers false for a string.

Also records where the absence tests' ten-minute virtual window stops proving
anything, and why the getTimerCount assertion is the period-independent one.
2026-08-26 13:25:13 +02:00
Lukas Moschitz 7c86d57fad fix(core): announce recovery's agent set, and protect knowledge not a status
Two fixes that the same test sequence exercises.

Recovery skipped onAgentsChanged for an unchanged agent set, to avoid core
re-subscribing the state manager and revoking the in-flight run's
subscription. That was the wrong lever: when an agent HAD been added — the
usual reason for restarting a runtime — the notification fired anyway and the
recovering run's state was lost, while an unchanged-set check that stopped
working would silently swallow a genuinely new agent. The subscription hazard
is now fixed at its source in state-manager, so recovery announces its set
like every other connection attempt.

hasLiveRuntimeKnowledgeToProtect required the status to be exactly Error, but
recovery deliberately passes through Connecting, so a setRuntimeUrl or
setRuntimeTransport landing in that window wiped the agents, closed the
submission gate and stranded the application red. The condition is now stated
in terms of the thing being protected: remote agents exist, and contact is not
currently established.

Also rewrites the two diagnosis assertions, which were pinned on a status code
interpolated into both branches and a phrase matching neither, so both halves
passed against the wrong branch.
2026-08-26 13:25:13 +02:00
Lukas Moschitz 5b30a8a5df fix(core): keep a live agent subscription when the same instance comes back
subscribeToAgent unconditionally tore down and revoked the subscription for
an agentId before installing a new one, including when it was handed the very
instance it was already subscribed to. The ag-ui pipeline captures its
subscriber list at run start, so the replacement is silently ignored and a run
that is still streaming loses its state and messages for the rest of the run.

Callers re-announce an unchanged agent routinely: a header change, a transport
change, an /info re-settle. Revocation now happens only when a DIFFERENT
instance takes over the id, which is the case the invariant was written for.
2026-08-26 13:25:13 +02:00
Lukas Moschitz db953a4857 fix(core): suppress a watched request's failure only when a check really ran
The watchdog set its "already reported" flag before calling the outcome
handler, which discards the report when a probe is already in flight, when
the status is not Connected, or when there is no runtime url. The flag then
claimed a report had happened and the request's own failure short-circuited
on a check that never ran, leaving the status green against a dead runtime.

The handler now reports back whether it actually started a check, and the
flag is set from that. A failure arriving after a check came back healthy is
new information, not part of the burst that check answered.
2026-08-26 13:25:13 +02:00
Lukas Moschitz 99863933a2 test(web-inspector): keep a utility name out of the tailwind source scan 2026-08-26 13:25:13 +02:00
Lukas Moschitz 4a7f8cf189 test(core): pin that a watched request's late answer still recovers
The watchdog reports silence and then suppresses the same request's eventual
FAILURE, so that one hang cannot buy two probes. Its eventual SUCCESS must
still count: the watchdog never cancelled anything, so a runtime that was
only very slow does answer, and that answer is contact — the one thing that
ends an outage. Widening the suppression to cover it would strand the status
at error with a working runtime.
2026-08-26 13:25:13 +02:00
Lukas Moschitz 58b7a7e850 refactor(core): drop the reachability probe cooldown
The cooldown armed only after a CONFIRMED-UNREACHABLE verdict, so with the
in-flight latch collapsing simultaneous failures and the "only probe while
Connected" guard swallowing everything while red, the single case it still
suppressed was a runtime flapping error -> connected -> failing again inside
the window. In that case the window is the damage: the second outage leaves
no trace, the status reads connected, and nothing is emitted.

Burst collapsing is unaffected and stays asserted — it is the in-flight
latch's job, and several simultaneous failures still cost exactly one probe.
The test that pinned absorption is replaced by its inverse: a crash-looping
runtime now gets a check per incident. Two tests derived their waits from the
constant and no longer need to; removing the wait also takes 2.5s off the
suite.

Also moves RUNTIME_REQUEST_WATCHDOG_MS out of agent-registry, which
core/index.ts re-exports wholesale, into utils/runtime-request, which nothing
re-exports — so the watchdog constant stays off the package index and this
branch's net public surface is one export smaller than before. Corrects the
probe-timeout comment that still claimed to be the only timer here.
2026-08-26 13:25:13 +02:00
Lukas Moschitz 8a6ca1aa04 fix(core): report a runtime request that never answers
The whole mechanism reacted to the OUTCOME of a request, so a runtime that
accepts the connection and never answers produced no outcome at all: nothing
was reported, no confirmation probe was started, and the status stayed green
indefinitely. Reproduced against the demo — a server that accepts TCP and
never writes, a message sent, System Health still reading "Healthy /
Available / Ready" after 35 seconds with the request still pending in the
network panel. The probe's own bound does not help; it bounds a probe, and
here no probe is ever started.

This is the common shape of the failures this feature names. Only a stopped
dev server refuses fast — a container mid-restart, a half-switched deploy and
a dropped tunnel all hang.

The instrumented fetch now arms a per-request watchdog. If no RESPONSE HEADERS
arrive within RUNTIME_REQUEST_WATCHDOG_MS the silence is reported as the same
suspected outage a failure reports, which runs the ordinary probe. It watches
headers, not the body, so an agent that thinks for a minute is untouched: the
fetch resolves on the response head and the SSE body streams afterwards.

The watchdog observes and never interferes. It does not abort or cancel the
request, so a merely slow runtime still completes the user's run and still
reports its eventual outcome. That outcome cannot buy a second probe if the
watchdog already reported for the same request — the in-flight latch and the
"only probe while Connected" guard do not cover that on their own, because a
probe that came back healthy releases both while the request is still open.

It does not arm during server rendering, nor for a request the caller declared
non-critical, nor for one the caller already bounds with its own timeout: such
a request cannot go silent, which is the only gap this fills, and a shorter
bound of ours would override the budget the caller chose. The timer is always
cleared when the request settles — the package has no disposal path.
2026-08-26 13:25:13 +02:00
Lukas Moschitz 13f7a3f042 test(core): pin that a cancelled thread request is still ignored
The caller-timeout fix works by telling two AbortErrors apart, so the
exclusion it must not weaken deserves its own case: the same hung runtime,
the same abort, but torn down by the caller rather than by its own clock —
and no check is issued.
2026-08-26 13:25:12 +02:00
Lukas Moschitz f657754703 fix(core): scope the re-sync ordering guard to recovery
The guard added with the rest of the fix round declined to paint the status
red whenever a success had landed after the attempt started. That is right
for the recovery re-sync, which has a follow-up attempt queued for exactly
that success — and wrong for a configuration change made from the red
state, which has nothing queued: the attempt returned without settling
anything and the status stayed at `connecting` for the rest of the page's
life. Found by working the case through rather than by a failing test, so
the test came first and reproduces it.

The three recovery-only behaviours (never remove an agent, do not
re-notify an unchanged agent set, let a later success overtake the verdict)
now hang off one `recovery` option instead of two flags that only ever
travelled together.

The ordering test is also sharpened. It asserted the final status, which
the queued follow-up made green either way — so it passed with the guard
deleted. It now asserts what a reader actually sees: no error status
notification and no wiring error for a runtime that has demonstrably been
answering. Verified: deleting the guard turns it red.
2026-08-26 13:25:12 +02:00
Lukas Moschitz 7f2353b1e6 test: cover the submission gate, the mid-session round trip and both transports
Three gaps the reviewers named, all of them behaviour the safety argument
already depends on.

The submission gate through the error state had no automated test in any
binding, though it is the third of three decisions that hold each other up:
if the red state closed the gate, no successful request could be issued and
only a page reload would leave it. The new react-core test drives the real
provider, the real core and the real submit path against a runtime that
goes away mid-session and asserts the state is left through the user
interface. Verified against a mutation that reuses the destructive startup
failure path mid-session: the test goes red.

A mid-session status round trip in a mounted tree was flagged as reasoned
rather than measured. Measured now: the run-activity effect lists the
status in its dependencies, so it does tear down and re-establish, but a
user-initiated run in flight is neither detached nor reconnected, and the
run-activity subscription is back once the status returns. When this chat
owns its run-activity store the round trip does restart it, re-issuing the
thread list and subscribe requests — documented rather than changed: it is
paid on a transition caused by user activity, not while idle.

Both connection-health suites pinned "rest" while the product default is
"auto". Core now covers rest, single and auto; the Intelligence suite
covers a runtime negotiated over the single-endpoint transport.
2026-08-26 13:25:12 +02:00
Lukas Moschitz 3eb89c9548 fix(core): bring the remaining runtime destinations onto the instrumented fetch
`applyRuntimeFetchToAgent`'s own comment claimed parity with
`applyHeadersToAgent`, but it was missing from `initialize`,
`addAgent__unsafe_dev_only` and `setAgents__unsafe_dev_only`. Worse,
`ProxiedCopilotRuntimeAgent` re-resolved its own runtime mode through a
bare global fetch, so an agent registered before the startup handshake
landed — the `registerProxiedAgent` case — asked `/info` off the seam and a
dead runtime stayed green. Those calls now go through `this.fetch`.

The stop request in `abortRun` stays on the global fetch and now says so:
it exists only because the user pressed Stop, and a user-initiated stop is
explicitly excluded from triggering a check. The comment and the code
agree either way.

Thread request timeouts were laundered into "cancelled". `threadFromFetch`
aborted its own controller when the store's 15s rxjs timeout unsubscribed,
and the resulting AbortError is indistinguishable from the user pressing
Stop, so a hung runtime produced no check at all. It now owns its timeout
and marks the request as caller-timed-out; genuine stop/unmount
cancellation, which still arrives via `signal` or unsubscription, is
classified exactly as before.

Callers can now mark a request non-critical, which the realtime-metadata
credentials route does: it is non-fatal by design, and older runtimes
refuse it outright because they do not offer the feature. Neither is news
about the runtime's health. A success on such a request still counts —
the runtime demonstrably answered.

The marker rides on `RequestInit`, which `fetch` ignores, so nothing
reaches the wire and no public API is added.
2026-08-26 13:25:12 +02:00
Lukas Moschitz cea5a16d03 fix(core): bound the reachability probe and stop losing a recovery
Four defects found in review, all in cases the harness could not express
because it modelled only a runtime that refuses.

The probe had no timeout and never released its latch. Against a runtime
that accepts the connection and never answers — a container mid-rollout, a
half-switched deploy, a dropped tunnel, i.e. this feature's own motivating
list — it never settled, every later failure short-circuited on the latch,
and the status stayed green indefinitely: the original bug, restored in
full. It is now raced against a bounded timeout that also aborts the
request, the latch is released in `finally` under a token so an abandoned
probe cannot clear a newer probe's latch, and a runtime url or transport
change abandons an in-flight probe outright.

The cooldown armed in that same `finally`, so it armed after a probe came
back HEALTHY — opening a window in which a genuine outage was dropped with
no probe, no log and no error. It now arms only on a confirmed-unreachable
verdict, and has tests either way; it had none at all before.

A success landing while a failed recovery re-sync was still notifying
subscribers collapsed onto that already-dying attempt, issued no `/info`,
and was lost — the chat visibly worked while the indicator stayed red for
the rest of the page's life. Recovery now queues instead of collapsing.
The in-flight comment claiming the two cannot overlap was wrong and is
corrected.

The recovery re-sync checked only `inspectorMetadataConnectionGeneration`,
which a successful runtime request does not bump, so a slow failing
`/info` could paint the status red after later runs had already been
answered. It now applies the same generation rule the probe does.

Also from the review:

- A configuration change made while the status is red reached the
  destructive path, wiping the agents, emptying the conversation and
  closing the submission gate — with no way back, since `connectRuntime()`
  only fires from `Disconnected`. That is a fourth site in the PRD's
  three-way safety argument and it now preserves.
- Recovery no longer removes agents. A runtime that is only part-way back
  answers truthfully that it is alive while listing few or no agents, and
  believing that report does the exact damage this design exists to
  prevent, through the success branch. Removal stays correct on a
  deliberate configuration change and on a fresh page load. With nothing
  being removed, an unchanged agent set no longer re-notifies
  `onAgentsChanged` — which was revoking an in-flight run's state-manager
  subscription mid-stream, since the re-sync runs while the run's response
  stream is still open.
- "Did not answer" and "answered, but refused" keep the same status and
  the same error code, and differ in wording and diagnostic context.
  Telling the reader a runtime that answered 401 is "unreachable" sends
  them to check addresses, ports and containers while the cause is a
  credential.
2026-08-26 13:25:12 +02:00
Lukas Moschitz cd448b2a0e test(core): let the connection-health harness express hung and part-way-back runtimes
takeRuntimeDown() modelled only a runtime that REFUSES, which is why every
defect at the edges of this feature survived the suite: a runtime that
accepts the connection and never answers, and one that answers /info with a
different or empty agent list, could not be written down at all.

The harness now covers both, independently for /info and the run routes,
and knows a second runtime url and the single-endpoint transport so a
configuration change can be exercised.

The two absence tests move to fake timers installed before the core exists
and advance ten minutes instead of observing a real 2.5s window. Verified
against a mutation: a 5s retry loop injected into the unreachable path is
now caught (123 requests observed where 3 are allowed) where the previous
real-timer window saw nothing.

RUNTIME_PROBE_COOLDOWN_MS is exported so waits derive from it.
2026-08-26 13:25:12 +02:00
Lukas Moschitz 1b252d1a26 fix(bindings): route thread requests through the instrumented runtime fetch
Thread REST calls go to the runtime, so under the destination rule they
are runtime traffic — but every binding injected the global fetch, so
their outcomes never reached the connection status. The practical effect:
with a dead runtime the Threads view left the status green, and after the
runtime came back, opening the Threads view could not clear it. Only
sending a message could.

React (both the useThreads store and CopilotChat's standalone
run-activity store), Vue, Angular and the Inspector's own owned store now
take `copilotkit.ɵruntimeFetch` instead. It is a pass-through and is
memoized per core, so nothing changes in the healthy case and no extra
request is issued.

Each binding's thread suite asserts the injection at the seam rather than
inferring it, since a regression back to the global fetch is invisible
from the rendered result.
2026-08-26 13:25:12 +02:00
Lukas Moschitz 215b72b622 fix(core): count the Intelligence runtime request towards connection health
Intelligence mode never touches the HTTP run route: it asks the runtime
over HTTP for realtime join credentials and then talks to the realtime
gateway over a websocket. That HTTP request used the global fetch, so it
bypassed the instrumented seam entirely and an Intelligence-mode
application still could not notice a runtime that went away — the one
case the destination rule ("a request counts if it goes to the runtime,
whatever issued it") was chosen to cover.

IntelligenceAgentConfig gains an optional `fetch`, defaulting to the
global one, and ProxiedCopilotRuntimeAgent hands its own request function
to the delegate it builds. Clones inherit it on both paths: the proxy's
clone already carries `fetch`, and IntelligenceAgent.clone passes the
whole config.

The websocket to the realtime endpoint deliberately stays out of it. It
is a separate service that can fail while the runtime is perfectly
healthy, and reporting "runtime unreachable" about a working runtime
costs more debugging time than no signal at all.
2026-08-26 13:25:12 +02:00
Lukas Moschitz e96c45aa31 fix(core): report the runtime connection status from the last actual contact
runtimeConnectionStatus was written once by the startup /info handshake
and never again, so a runtime that died after page load kept reporting
"connected" until a reload: System Health, the launcher error signal and
customer onError handlers all said healthy while nothing worked, and the
failure surfaced only as a failed agent run.

Every proxied runtime agent now routes its requests through an
instrumented fetch that reports each outcome. A failed request asks the
runtime once, directly, whether it is there (reusing the /info request,
never the startup connection path); if it is not, the status moves to
error, subscribers are notified and RUNTIME_INFO_FETCH_FAILED is emitted
with the runtime url, alongside the run failure. A successful request
moves it back and re-runs the full handshake so agents and version are
reconciled. There is no timer, no polling and no retry loop; a 2s
cooldown collapses a burst of failures into one probe, and a generation
counter keeps a late verdict from overriding an earlier success.

Both the inbound transition and a failed recovery re-sync preserve every
piece of runtime knowledge. Conversation state lives on the agent
instance and submission is gated on an agent being bound, so discarding
the agents would empty the chat and close the only path back out of the
error state.
2026-08-26 13:25:12 +02:00
Lukas Moschitz efe5199bfa refactor(core): one definition of abort detection
The run handler matched four abort signals inline while the agent module
kept its own name-only predicate. Extract the superset (the AbortError
name plus the fixed message strings) into utils/abort-error.ts so every
place that classifies a failed request agrees on what a cancellation is.
2026-08-26 13:25:12 +02:00
Alem Tuzlak fd7f2fa683 Merge origin/main into alem/hud-arrow-color 2026-08-26 11:36:50 +02:00
Tyler Slaton a6751df682 fix(web-inspector): theme launcher HUD with inspector 2026-08-26 03:40:30 +02:00
Tyler Slaton d2e0c35cae fix(web-inspector): align launcher HUD shell styling 2026-08-26 03:38:32 +02:00
Tyler Slaton d2a6eee07a fix(web-inspector): align launcher HUD availability 2026-08-26 03:29:38 +02:00
tylerslaton 9629e930d1 chore: release monorepo v1.69.2 2026-08-26 00:18:42 +00:00
Tyler Slaton b3b339f544 Revert "feat(web-inspector): add Event Snippets and save-as-snippet (#6649)"
This reverts commit ba4260ad66, reversing
changes made to 47c5510b49.
2026-08-26 02:11:19 +02:00
Tyler Slaton dc916484ab fix(inspector): clarify local action availability
fix(inspector): group local developer actions

fix(inspector): refine local tools menu

fix(inspector): use direct local action
2026-08-26 02:11:19 +02:00
Mike Ryan b3c3cb0d7b test(web-inspector): stop the gesture tests racing the beat on real timers (#6693)
## Problem

`packages/web-inspector/src/__tests__/launcher-error-signal.spec.ts` is
failing intermittently on `test / unit`, across unrelated branches and
on `main`. It is currently red on the **v1.69.1 release PR**.

```
FAIL  src/__tests__/launcher-error-signal.spec.ts >
      the whole gesture completes on its own and leaves the resting state behind
AssertionError: expected 'opening' to be 'closed'
```

| When | Branch | Shard | Run |
|---|---|---|---|
| 19:29Z | `release/publish/monorepo/v1.69.1` | Node 24 / React 18 |
[32886136479](https://github.com/CopilotKit/CopilotKit/actions/runs/32886136479)
|
| 14:09Z | `ben1/oss-924-agui-core-058` (#6687) | Node 24 / React 19 |
[32857130830](https://github.com/CopilotKit/CopilotKit/actions/runs/32857130830)
|
| 12:41Z | `lukas/oss-903-presentation-…` | Node 20 / React 18 |
[32848616256](https://github.com/CopilotKit/CopilotKit/actions/runs/32848616256)
|

Neither #6687 (an `@ag-ui/core` version bump) nor the presentation
branch touches gesture timing, and no shard fails consistently — it
follows runner load, not code.

## Cause

Both real-timer tests in this suite asserted a *pre-beat* state 200ms
after breaking the connection:

```ts
const context = await setup({ realTimers: true });
await context.breakConnection();
await context.advance(200);
expect(pillPhase(context.inspector)).toBe("closed");   // ← races the beat
```

Under `realTimers`, `advance(ms)` is a literal `setTimeout(resolve, ms)`
(spec L656–664). The pill's `closed → opening` transition fires at
`ERROR_GESTURE_MS.beat = 400` (`index.ts` L330–339, scheduled at
L18998). So the assertion had a **200ms margin against a 400ms boundary
on a wall clock**. On a loaded runner the 200ms sleep overshoots 400ms,
the beat has already fired, and the phase reads `opening`.

The comment directly above the test already says phase boundaries are
asserted on the fake clock "because real timers would make this suite
slow and flaky" — and then this was a phase-boundary assertion on real
timers.

The same 200-vs-400 race sat in the adjacent test (`pulsing` is true
only for the beat's 400ms), so both are fixed here.

## Fix

Remove the two racy preconditions. Both claims are already pinned
deterministically on the fake clock at spec L411–427, which asserts
`pillPhase === "closed"` **and** `pulsing === true` right after arming,
then walks every phase boundary. These real-timer tests exist only to
show the beat and the gesture run to their end on their own — which the
loops and their closing assertions still prove.

This also matches the idiom the sibling `launcher-signal.spec.ts`
already uses (L667–676): assert at t≈0, then poll for the end.

## Testing

**1. Reproduced the CI failure locally.** Injected a 250ms stall before
the assertion on the unmodified test, simulating a loaded runner (200ms
sleep + 250ms ≈ 450ms > the 400ms beat):

```
FAIL  src/__tests__/launcher-error-signal.spec.ts > the whole gesture completes on its own …
AssertionError: expected 'opening' to be 'closed' // Object.is equality
Expected: "closed"
Received: "opening"
 ❯ src/__tests__/launcher-error-signal.spec.ts:2666:40
```

Byte-for-byte the CI assertion.

**2. The fix survives that same simulation.** With a 700ms stall (well
past the beat) injected into both tests:

```
 ✓ the beat ends and leaves the resting dot behind  750ms
 ✓ the whole gesture completes on its own and leaves the resting state behind  3435ms
 Tests  2 passed | 88 skipped (90)
```

**3. Mutation-checked that the remaining assertions still have teeth.**
Three separate breaks to `src/index.ts`, each caught:

| Mutation | Result |
|---|---|
| Gesture opens but never closes (drop the `closing` phase +
`endGesture`) | `FAIL … AssertionError: expected <span …> to be null` |
| Pill never opens at all (`openPill` returns early) | `FAIL …
AssertionError: expected false to be true` (`sawOpenPill`) |
| Beat never ends (`beat: 400` → `999_999`) | `FAIL … AssertionError:
expected true to be false` (`pulsing`) |

Source restored afterwards; `git status` confirms this PR touches only
the spec file.

**4. Full `@copilotkit/web-inspector` suite:**

```
 Test Files  29 passed (29)
      Tests  625 passed (625)
```

**5. Pre-commit gate** (`test-and-check-packages`: test, publint, attw
across 5 projects + 22 dependencies, incl. `@copilotkit/react-core`,
`@copilotkit/angular`, `@copilotkit/runtime`) passed on the committed
tree.

## Note

`main` also has a second, unrelated flake I did not touch here —
`CopilotChatToolRerenders.e2e.test.tsx > should not re-render a
completed tool call when subsequent text is streamed` (`expected 4 to be
3`), which reddened `main` at `0943c519` ([run
32750372113](https://github.com/CopilotKit/CopilotKit/actions/runs/32750372113)),
a runtime `.d.ts` change that touches no react-core chat code. Different
mechanism, worth its own issue.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-25 14:22:03 -07:00
Benjamin Taylor b8283ef4e1 refactor(scripts): match dead hosts case-insensitively, carry each reason with its host
DNS is case-insensitive, so a capitalized host in prose would have slipped the
literal match. Env var names stay case-sensitive — `ignoreCase` is opt-in per
rule. The per-host reason moves onto the constant so adding a third host cannot
silently inherit the wrong message.

Also restores the TSDoc's original framing of what an override is for
("non-production or future self-hosted"), matching the runtime skill's wording
rather than diverging from it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 15:45:27 -05:00
Benjamin Taylor b057744684 fix(docs): stop shipping stale Intelligence config claims, and gate the dead hosts (refs OSS-961)
The packaged runtime skill up to v1.62.2 prescribed `api.copilotkit.ai` /
`realtime.copilotkit.ai`. The first host is a CNAME onto the legacy Copilot
Cloud ALB, where no listener rule matches it, so every request gets the ALB
default action: a 404 with an empty body. The second has no DNS record at all.
A reader who followed that page converted a working OSS install into a 502.

The hosts themselves were corrected in v1.64.0, but two shipped surfaces still
carried stale claims about the same step, and nothing stopped the hosts from
coming back a third time:

- The debug skill said Intelligence "requires ... `apiUrl`, `wsUrl`, `apiKey`,
  `tenantId`". Three errors in one line: `apiUrl`/`wsUrl` have been optional
  with managed defaults since v1.64.0, and `tenantId` has never existed on
  `CopilotKitIntelligenceConfig` — the API key carries the project (its token
  format is `cpk-{projectId}_...`) and the platform resolves the organization
  server-side, so there is no org or tenant field for a caller to pass.
- `CopilotKitIntelligence`'s own TSDoc showed only `*.internal` placeholders,
  so the class's hover docs never named the pair that actually serves prod.

`validate-intelligence-env-names` — already the unfiltered guard for this same
config surface (OSS-881) — now also fails on either dead host. The
channels-intelligence realtime test is allowlisted: it needs a hostname that
genuinely does not resolve, since `getaddrinfo ENOTFOUND` is the condition
under test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 15:35:55 -05:00
Tyler Slaton 79c02f0f02 style(web-inspector): fewer layers on the launcher, a lens for its dot (#6688)
## What does this PR do?

Design review on the launcher and its notification dot asked for three
things: a milder face than solid black, fewer borders and background
layers, and a subtle shadow in place of the dot's heavy border. This is
all three, plus the removal of six utilities that never had any effect.

Everything here was compared side by side at production size, on a light
*and* a dark host page, before it was chosen. Two of my own first
proposals were dropped after measuring them, both described below.

**Two concerns, three commits.** `4fa38d91f` and `cd711b369` are the
launcher itself — the package change, 2 files. `295f75495` gives the
react-router lab a dark mode, because a dark host page is what this
change has to be judged against and the lab could not produce one. If
you would rather review those separately, say so and I will split them.

## The face

`#181C1F` at 95%, which review asked for.

Worth recording so it does not come up again: the near-black the review
saw was `#010507`, 20.5:1 against white. What shipped yesterday was
already `#1C1F24` at 16.5:1, so this value is a hair *darker* than the
one it replaces (17.2:1) and the difference between them is a ΔE of 2.3,
at the floor of what an eye can separate. It settles the question rather
than changing the look.

## Fewer layers

Six Tailwind utilities on the launcher set properties the unlayered
`css` block sets again — `bg-slate-950/95`, `border-white/20`, `ring-1`,
`ring-white/10` and the two hover variants. Unlayered declarations beat
layered ones regardless of specificity or source order, so none of them
has ever had any effect. Each was the package's only use, so the
checked-in stylesheet drops 980 bytes.

Of the *visible* layers, two went:

**The outer hairline.** The launcher carried two concentric lilac rings:
the border, and a second one 1px outside it as a box-shadow. The outer
one also hardcoded the lilac rather than reading `--cpk-launcher-edge`,
so it silently could not follow the token. It is replaced by a one-pixel
light edge along the top, which is what keeps the face from reading flat
without drawing a frame.

**`backdrop-blur-md`.** It sat behind a 95%-opaque fill and bought close
to nothing, while mounting a permanent blur compositing layer over a
customer's page.

**The border stays, and this is the finding that changed my mind.** I
first proposed removing it too. Against a dark host page the face
measures 1.10:1 (GitHub dark), 1.04:1 (Tailwind slate-900) and 1.22:1
(black) — indistinguishable from the page. The border is the only thing
that gives the launcher an outline there. It is not decoration.

## The dot

The collar was `1.5px`, opaque, zero blur, and 21% of the dot's
footprint. Because the dot's centre sits *on* the rim, its outer half
painted a hard dark crescent onto the **host page** rather than onto the
launcher — which is what read as "heavy". A hairline plus a soft drop
separates it just as well.

The fill becomes a lens lit from the upper left. Both stops are derived
from `--cpk-launcher-signal`, so a new tone needs no new values;
verified for the rose error tone and the violet announcement tone.

**Dropped after looking at it:** a coloured glow around the dot. It was
the obvious reading of "more premium", but the launcher already pulses
in that same colour when a failure is new, and a permanent glow competes
with the thing that is supposed to draw the eye.

**Also dropped:** tinting the border in the signal colour, which was
suggested in review. On a dark page the border is the entire silhouette,
so tinting it recolours the whole launcher for a state that can persist
for hours.

## One non-obvious consequence

Removing the blur removed a side effect nobody had written down:
`backdrop-filter` promotes the element to its own compositing layer.
Without a layer, the hover `scale(1.05)` re-rasterises the mark every
frame and it visibly jitters — geometrically nothing moves, the mark's
centre holds to three decimals, but the vector is re-rendered at
fractional offsets. `will-change: transform` asks for the layer directly
and the jitter is gone. Confirmed by eye on the running demo before this
was chosen.

## Tests

`packages/web-inspector` stays at **28 files / 611 tests**, all passing.

No new tests. The colour tests here are deliberately token-shaped rather
than value-shaped — they assert the custom property and the *sharing* of
one face and one edge between the launcher and its pill, never a hex —
so face and edge values are free to move and this change is exactly the
kind they were written to allow. The one test that constrains it, `"the
pill and the launcher share one surface and one edge"`, still passes.

What is genuinely unguarded, and was before this PR too: the dot's
collar width, the double hairline, and the Tailwind class list.
Asserting rendered geometry would need a browser test runner, which this
package does not have — jsdom computes no layout.

## How to see it

`pnpm --filter react-router-example dev`, then `http://localhost:5173`.
The launcher is top right; `Break runtime` arms the error tone and
`Break run` the announcement one. Hover it to check the mark no longer
jitters.

One thing worth knowing while reviewing: the launcher anchors top-right
and is `position: fixed` on an element mounted directly under `<body>`,
so on this page it sits over the lab's toolbar. Drag it to the lower
right and it is out of the way.

## The lab's dark mode

A dark host page is where the launcher's border earns its place, and the
lab had no way to produce one, so reviewing this change was not possible
without it.

It follows `examples/v2/react/demo` rather than inventing anything: the
host owns a `theme` state, and `CopilotChat` gets `className="dark"` —
which is what makes the package swap its own variable set. The colours
are the demo's by another route; it writes the oklch literals
CopilotKit's variables use, and those are Tailwind's neutral steps
(`neutral-950` is `oklch(0.145 0 0)`, `neutral-50` is `oklch(0.985 0
0)`, `neutral-800` is `oklch(0.269 0 0)`). Measured identical on the
running lab.

`@custom-variant dark (&:is(.dark *))` is needed in the lab's stylesheet
because Tailwind v4 points `dark:` at `prefers-color-scheme` by default,
so the toggle would have lost to the OS. Same declaration the package
uses for its own sheet.

Two details that are decisions rather than oversights. The **error
banner keeps a rose tint** in dark mode instead of going neutral,
because an error banner that looks like every other surface is not an
error banner. And the **toolbar buttons keep a visible on/off contrast**
— active inverts to a light face, inactive sits on `neutral-800` —
because the lab's whole purpose is knowing which failure is currently
armed.

My first attempt stripped every background instead of theming, and that
is worth recording because it looked plausible: the chat bubble, the
send button, the button states and the banner all collapsed into one
flat grey. The chat paints its own surfaces and has to be told what
theme it is in, not undressed.

## A separate bug found on the way

`CopilotKitProvider` documents `inspectorDefaultAnchor` — *"Default
anchor corner for the inspector button and window"* — and it has no
effect. `defaultAnchor` is typed on the React wrapper and forwarded to
the element, but the string `defaultAnchor` does not occur anywhere in
`packages/web-inspector`, so it lands as `defaultanchor="[object
Object]"` and is ignored. The corner stays hardcoded `{ horizontal:
"right", vertical: "top" }` in two places.

Not fixed here, to keep this PR to one concern. It is worth fixing: any
host with a top navigation bar hits exactly this, finds exactly that
prop, and it does nothing.

## Related PRs and Issues

- Follows #6646
2026-08-25 13:27:06 -07:00
Benjamin Taylor 04684614ca test(web-inspector): stop the gesture tests racing the beat on real timers
The two real-timer tests in the launcher's error-signal suite each asserted a
pre-beat state 200ms after breaking the connection. The beat is 400ms
(ERROR_GESTURE_MS.beat), so both assertions had a 200ms margin against a wall
clock on a shared runner. When the runner is loaded the 200ms sleep overshoots
400ms, the beat has already fired, and the assertion reports the next phase.

Both claims are already pinned deterministically on the fake clock, where every
phase boundary of the gesture is asserted. The real-timer tests exist only to
show the beat and the gesture run to their end on their own, so the racy
preconditions are removed rather than retimed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 14:46:25 -05:00
MikeRyanDev 6053e4e262 chore: release monorepo v1.69.1 2026-08-25 18:50:37 +00:00
Alem Tuzlak 7509b379e6 fix(web-inspector): remove extra space under the sidebar plan card
The sidebar footer had 18px extra padding under the plan card.

The card now uses the same 12px inset as the rest of the sidebar.
2026-08-25 18:36:29 +02:00
Alem Tuzlak 72f037744f fix(web-inspector): match HUD arrow and drop the SW overlay
The last mix made the HUD arrow a bit too light.

An empty 28px SW resize handle sat on the collapsed sidebar toggle.

In dark mode that handle painted as a black box.

The south and west edges still resize the floating window.

The SE grip is unchanged.
2026-08-25 18:29:55 +02:00
Alem Tuzlak 841aea0614 style(web-inspector): lighten the HUD arrow to match the glass card 2026-08-25 18:12:10 +02:00
Alem Tuzlak f7efb5dc7b style(web-inspector): keep the HUD glass fill and match the arrow to it 2026-08-25 18:08:44 +02:00
Alem Tuzlak 3d5d42c1a0 style(web-inspector): match the HUD arrow fill to the card 2026-08-25 18:04:34 +02:00
Aswin Kumar 042a098ae6 Merge branch 'main' into fix/angular-hitl-result-envelope 2026-08-25 20:30:59 +05:30
Lukas Moschitz cd711b369c style(web-inspector): one hairline on the launcher, a lens for the dot
Review asked for fewer borders and boxes, and for the dot's heavy border to
become a subtle shadow.

The launcher had two concentric lilac hairlines: the border, and a second
ring 1px outside it as a box-shadow, which also hardcoded the lilac instead
of reading the edge token. The outer one goes. What replaces it is a
one-pixel light edge along the top, which keeps the face from reading flat
without drawing a frame.

The border itself stays, and it is not decoration: against a dark host page
the face measures 1.10:1 (GitHub dark) and 1.04:1 (Tailwind slate-900), so
without it the launcher has no outline there at all.

`backdrop-blur-md` goes too. It sat behind a 95%-opaque fill and bought
almost nothing, while mounting a permanent blur layer over a customer's
page. It did have one real side effect -- promoting the launcher to its own
compositing layer -- and without that the hover scale re-rasterises the mark
every frame and it visibly jitters. `will-change: transform` asks for the
layer directly, and the jitter is gone.

The dot loses its opaque 1.5px collar. That collar was 21% of the dot's
footprint, and because the dot's centre sits *on* the rim, its outer half
painted a hard dark crescent onto the host page rather than onto the
launcher -- which is what read as "heavy". A hairline plus a soft drop
separates it just as well. The fill becomes a lens lit from the upper left,
both stops derived from the signal colour so a new tone needs no new values.

Considered and dropped: tinting the border in the signal colour. On a dark
page the border is the whole silhouette, so tinting it recolours the entire
launcher for a state that can persist for hours -- and the launcher already
pulses in that colour when the failure is new.
2026-08-25 16:42:54 +02:00
github-actions[bot] 3667885fd1 style: auto-fix formatting 2026-08-25 14:08:09 +00:00
Alem Tuzlak 6870c4a926 test(react-core): give the 100-message chat render a 20s test timeout 2026-08-25 16:06:37 +02:00
Aswin Kumar 8eb55a118e Merge branch 'main' into fix/angular-hitl-result-envelope 2026-08-25 19:25:45 +05:30
Alem Tuzlak 3d23675968 test(web-inspector): type HUD helpers as Node so check-types passes 2026-08-25 15:52:03 +02:00
Lukas Moschitz 4fa38d91f2 refactor(web-inspector): drop the launcher's dead chrome and soften its face
Six Tailwind utilities on the launcher set properties the unlayered `css`
block sets again, and unlayered declarations beat layered ones regardless of
specificity -- so `bg-slate-950/95`, `border-white/20`, `ring-1`,
`ring-white/10` and the two hover variants have never had any effect. Each
was the package's only use, so the checked-in stylesheet drops 939 bytes.
No visual change: verified the three rules are gone from the generated sheet
and the 611 tests still pass.

Kept deliberately: plain `border` (the hand CSS sets only `border-color`),
`rounded-full` (nothing else sets the radius) and the focus-visible trio
(the hand CSS sets only `outline-color`).

The face moves to `#181C1F` at 95%, which review asked for. It is a hair
darker than the `#1C1F24` this replaces -- 17.2:1 against white rather than
16.5:1, a ΔE of 2.3, at the floor of what an eye can separate -- so this
settles the question rather than changing the look.
2026-08-25 15:49:55 +02:00
github-actions[bot] ec8ad0c33a style: auto-fix formatting 2026-08-25 13:30:54 +00:00
Alem Tuzlak 7dd47cc2dd Merge branch 'main' into alem/oss-903-inspector-bubble-emanations 2026-08-25 15:28:55 +02:00
Tyler Slaton c61f0f32ed feat(web-inspector): surface failures on the launcher and open the Inspector on them (#6646) 2026-08-25 06:28:17 -07:00
Ben Taylor d85b0b5db9 fix(runtime): reject runner passed alongside intelligence (closes OSS-933) (#6670)
## Problem

`runner` and `intelligence` are mutually exclusive by construction, but
the exclusivity was enforced in only one direction and only for object
literals.

`CopilotIntelligenceRuntime` hardcodes `new
IntelligenceAgentRunner(...)` into its `super()` call
(`runtime.ts:582`), and `runner?` is declared only on
`CopilotSseRuntimeOptions` (`runtime.ts:239`). The type system catches a
`runner:` key on an Intelligence-shaped **object literal** via
excess-property checking — but that is the only barrier. A JS caller, an
`as any`, or a non-literal options object routes through
`CopilotRuntimeShim`'s `hasIntelligenceOptions()` dispatch into the
Intelligence constructor and has `runner` **silently dropped**, with no
diagnostic.

The mirror case is already guarded: `CopilotSseRuntime` throws on
`channels`, and the comment there states the exact reasoning that
applies here — "the type forbids it, but a JS / `as any` caller ...
would otherwise land here and have `channels` silently dropped — fail
loud instead." The Intelligence constructor validates `identifyUser`,
`channels`, `memory`, and `ɵlearning`. Same file, same pattern, one case
missing.

### It also made a shipped skill lie

`packages/runtime/skills/runtime/SKILL.md:87` asserted:

> Passing both `runner` and `intelligence` to `CopilotRuntime` is
rejected at construction.

It was not. And that contradicted the skill's own reference page,
`references/agent-runners.md`, which correctly described the silent
drop. Two files in the same shipped skill said opposite things about the
same behaviour.

## Change

- **Guard** (`runtime.ts:512`) — `CopilotIntelligenceRuntime` now throws
when `runner` is present, mirroring the `channels` guard in
`CopilotSseRuntime`. The message names the exclusivity and points out
that an in-memory/SQLite runner is unnecessary in Intelligence mode,
where durability is managed by the service.
- **`SKILL.md:87` unchanged** — the guard makes it accurate.
- **`references/agent-runners.md` updated** — this is *not* optional.
That page was the accurate one before this change; adding the guard
makes its "the auto-wired Intelligence runner wins regardless of what
you pass" false. Leaving it would fix SKILL.md's lie by creating the
same lie in the reference — rotating the contradiction rather than
resolving it. Its stale `:149-173,285-294` source citation is corrected
to the real line numbers too. Root `skills/` mirror regenerated via
`pnpm sync:plugin-skills`.

## Behavior notes

- Explicit `runner: undefined` still constructs. This matches how the
sibling `identifyUser` / `channels` / `memory` guards treat `undefined`,
and avoids breaking callers that spread an options object.
- The v1 deprecated compat path is unaffected:
`copilot-runtime.ts:492-509` already omits `runner` from its
Intelligence branch, so nothing routes a `runner` into this constructor
from v1.

## Tests

Two tests in `channels-option.test.ts`, alongside the existing `sse
runtime rejects channels` mirror:

- `intelligence runtime rejects a caller-supplied runner` — written
first and confirmed **red** against the unpatched constructor
(`AssertionError: expected [Function] to throw an error`), green after.
- `intelligence runtime tolerates an explicitly undefined runner` — pins
the undefined-tolerance above so the guard cannot over-throw.

## Verification

| Gate | Result |
|---|---|
| `nx test @copilotkit/runtime` | 144 files, **2080 passed, 0 failed** |
| `nx check-types @copilotkit/runtime` | Successfully ran (+22 deps) |
| `oxlint` (changed files) | 0 warnings, 0 errors |
| `oxfmt --check` | no issues in changed files |
| lefthook pre-commit + commit-msg | all green |

Closes OSS-933.
2026-08-25 08:22:06 -05:00
Alem Tuzlak 3a3df106b5 fix(web-inspector): show Learning on or off in the HUD 2026-08-25 14:53:30 +02:00
Lukas Moschitz 23e3287fa1 test(web-inspector): stop the privacy check tripping over a random id
"no telemetry payload anywhere carries the failure message" serialises the
whole property bag and asserts it does not contain "503". Two of those
properties are the anonymous distinct id -- random hex -- so a three-digit
numeric needle lands inside one roughly once in a few hundred runs. It just
failed a CI job that way, which reads like a privacy breach and is not one.

The ids are excluded by name and asserted to still be strings, rather than
weakening the needles, which are the point of the test. Proven both ways:
with an id seeded to contain "503" the test passes with this change and
fails without it, and it still goes red when a real message is attached to
the payload.
2026-08-25 14:49:17 +02:00