## What's broken
Every GitHub Release this repo has ever cut has a body of `Release
<tag>` and nothing else — `v1.70.0`, `v1.69.3`, `channels/v0.6.0`,
`angular/v0.4.0`, all of them. The `#engr` Slack announcement links
"Release notes" at that page, so that link has always pointed at a blank
release.
The notes *were* being generated. The `angular/v0.5.0` create-pr run
logged:
```
Raw release notes written to release-notes.md
Generating AI-enhanced release notes...
AI-enhanced release notes written to release-notes.md
```
They just never left the runner. `release-notes.md` was gitignored
(`.gitignore:72-73`), so `peter-evans/create-pull-request` skipped it,
the file never reached the release branch, and `publish-release.yml`'s
`readFileSync("./release-notes.md")` missed and fell through to `body =
\`Release ${name}\``.
The same ignore rule severed the Notion round-trip:
`release-notes-notion.json` was ignored too, so the publish job could
never read an edited draft back. That path had never run either.
Meanwhile the repo carried **29 changelog files that no tooling had
written since April**. They are changesets-era leftovers, and nothing in
`scripts/` or `.github/` reads or writes them:
```
$ git grep -n "CHANGELOG" -- 'scripts/**' '.github/**' '*.json' ':!*/CHANGELOG.md'
(no matches)
```
They stopped at `1.55.2` while the monorepo lane shipped `1.69.3`, and
`packages/angular/CHANGELOG.md` still claimed `1.54.3` — a version from
before angular split onto its own `0.x` line. So the only changelog a
reader could find in the tree named the wrong version for the wrong
lane.
## What this changes
**1. The notes become a source-controlled changelog, one file per
release lane.**
| lane | file |
|---|---|
| `monorepo` | `CHANGELOG.md` |
| `angular` | `packages/angular/CHANGELOG.md` |
| `channels` | `packages/channels/CHANGELOG.md` |
Per lane rather than one root file because the lanes version
independently: a shared file would interleave `1.70.0`, `angular/0.5.0`
and `channels/0.9.0` into one sequence where no reader can follow any
single line. (Concurrent writes are *not* the reason —
`stable-release.yml` already fails if any release PR is open.)
The flow: `prepare-release.ts` writes the raw notes,
`generate-ai-release-notes.ts` polishes them, **`write-changelog.ts`**
prepends them as this version's section, `create-pull-request` commits
the changelog (a tracked file, so `git add -A` always stages it), and
**`extract-release-notes.ts`** reads that section back in the publish
job as the GitHub Release body.
The changelog is therefore both the durable record and the review
surface: edit the top section on the release PR to change what ships.
`release-notes.md` goes back to being gitignored scratch, so the same
notes never exist as two editable copies with no rule about which one
wins.
**2. The 29 stale changelogs are deleted**, and a test pins the tracked
changelog set to exactly the three lane files, so they cannot creep back
and contradict the real versions again. Their content stays recoverable
from git history (`git show v1.69.3:packages/core/CHANGELOG.md`).
**3. Notes are selected per PR, scoped to the lane.** Selection was
`--no-merges` over every commit since the scope's tag. Two bugs:
- *No path filter* — a scope inherited every other lane's work.
- *`--no-merges` is backwards here* — this repo merges PRs as merge
commits, so the merge **is** the unit of change and the only commit
carrying `(#1234)`. `--no-merges` dropped every PR boundary and kept the
intermediate branch commits.
Now: `--first-parent` over the scope's package directories, minus
commits no consumer would read about (`test`/`ci`/`style`, `chore`
except `chore(deps)`, and the release commit itself).
| scope | before | after |
|---|---|---|
| `angular` v0.5.0 | 159 entries | **4** |
| `channels` (unreleased) | 600+ entries | **9** |
**4. Breaking-change footers still survive.** `--first-parent` alone
silently dropped `BREAKING CHANGE:` footers written on branch commits
rather than in the PR description — measured at **2 of 2 lost** across
`v1.60.0..HEAD`. Each merge's branch messages are now folded into its
body before extraction, so the entry list stays one-per-PR while the
footer scan sees the whole PR. Re-measured: **0 lost**.
**5. The AI prompt is scoped and the API call is correct.** It was
passing a repo-wide `git log -50` as "context" and asserting the release
was "CopilotKit vX.Y.Z, an open-source AI agent framework for React
applications" — wrong commits, wrong framing, and wrong release title
for any non-monorepo lane. Now it gets the lane's own commits, the names
of the packages actually being published, and an instruction to write
about nothing else. Also fixed in the same call: `max_tokens: 2048`
(truncates a large release mid-section, and the truncated text is what
ships as the body), and a response reader that took `content[0].text`
rather than selecting the text block by type. The model pin is left
alone — `main` already carries a current, undated id.
**6. Notion is removed**, not repaired — the release PR is already the
review surface.
### Failure behavior on the publish side
`extract-release-notes.ts` runs **after** `npm publish`, so it never
exits non-zero: failing there would leave the packages published and the
tag unpushed. A missing section prints a `::error::` annotation and
falls through to the workflow's existing `Release <tag>` fallback. Worst
case is the blank body we have today, never a half-finished release.
## Testing
Baseline on `main`: `15 files / 162 tests`. On this branch: **`16 files
/ 197 tests`**.
```
$ npx vitest run --config scripts/release/vitest.config.mts
Test Files 16 passed (16)
Tests 197 passed (197)
```
**The whole lane round-trips end to end.** A real `prepare-release.ts
--scope channels --bump minor` run (versions reverted afterward), then
the two new halves:
```
$ pnpm tsx scripts/release/write-changelog.ts 0.10.0 channels
Recorded 0.10.0 in packages/channels/CHANGELOG.md
$ rm release-notes.md
$ pnpm tsx scripts/release/extract-release-notes.ts 0.10.0 channels
Release body written to release-notes.md from packages/channels/CHANGELOG.md (861 chars)
```
The extracted body is the 9 PR-numbered entries under Features / Fixes /
Other, with **no duplicated version heading** (`grep -c '^## '
release-notes.md` → `0`) — the raw generator's own `## v0.10.0
(channels)` line is stripped when the section heading is written. The
miss path was exercised too:
```
$ pnpm tsx scripts/release/extract-release-notes.ts 9.9.9 channels
::error title=Release notes::No section for 9.9.9 in packages/channels/CHANGELOG.md. ...
exit: 0
```
**The staging behavior is verified against the pinned action, not
assumed.** `peter-evans/create-pull-request@5f6978f` stages with `git
add -A` when `add-paths` is unset. In-repo, after a real notes run:
```
$ git add -A --dry-run | grep -iE "changelog|release-notes"
add 'packages/channels/CHANGELOG.md'
$ git check-ignore -v release-notes.md
.gitignore:77:release-notes.md release-notes.md
```
The changelog is staged; the scratch file is invisible to the commit.
The publish job checks out `ref: main` at `fetch-depth: 0`, and the
release PR merges the changelog into main, so the section is present
when the extractor runs.
**The selection reproduces a hand-curated list exactly.**
`angular/v0.5.0`'s release body was written by hand from its four real
PRs. Running the new selection over that same range returns exactly
those four, release commit correctly dropped:
```
#6098 feat(runtime): use managed Intelligence authority (#6098)
#6756 chore(deps): bump @ag-ui/* to 0.0.59 (#6756)
#6773 feat(angular): add registerComponent ... (refs OSS-1034) (#6773)
#6586 fix(angular): resolve human-in-the-loop results without the bus envelope (#6586)
```
**Breaking-change regression measured, not assumed** — differential
comparison of extracted notes, old selection vs new, over two ranges:
```
range v1.60.0..HEAD old: 2 new: 2 LOST: 0
range v1.50.0..HEAD old: 2 new: 2 LOST: 0
```
(Before the fold was added, the same probe reported `LOST: 2` — that is
how the bug was caught.)
**Every new test was mutation-checked** — the mechanism was broken and
the test confirmed failing:
| mutation | result |
|---|---|
| `--first-parent` → `--no-merges` | 2 failed |
| drop the pathspec filter | 2 failed |
| `isNoiseCommit` always false | 2 failed |
| `parsePrNumber` always null | 2 failed |
| `withBranchMessages` → no-op | 1 failed |
| code-fence tracking disabled | 1 failed |
| `stripVersionHeading` → no-op | 3 failed |
| `prependSection` appends instead | 1 failed |
| `extractSection` keeps the heading | 5 failed |
| `upsertSection` stops replacing | 1 failed |
| re-ignore a lane changelog | 1 failed |
| re-ignore all `packages/*/CHANGELOG.md` | 2 failed |
| an orphan changelog creeps back | 1 failed |
| a lane changelog goes missing | 1 failed |
| *(restored)* | **all green** |
One of those mutations found a bug **in the test itself**: `git
check-ignore <path>` reports nothing for a path that is already tracked,
so the ignore assertion passed against a rule that would still strand
the next lane's file. It now runs `git check-ignore --no-index`, and the
mutation fails as it should. The flagless form is why the row above
exists at all.
Also run: `verify-release-scope-dropdowns.sh` (all OK), YAML parse of
both edited workflows, `oxfmt` (no-op after formatting), `oxlint` (0
warnings, 44 files).
**Not verified:** the live Claude API call. No `ANTHROPIC_API_KEY` was
available locally, so only the no-key fallback path (raw changelog) and
the CLI arg validation were exercised. A generation failure is already
caught and falls back to the raw notes, so the worst case is un-polished
notes rather than a blank body.
The commit is `--no-verify`: the pre-commit nx lane cannot run in this
worktree (`packages/core` and `packages/channels-ui` have no
`node_modules`, and `nx run @copilotkit/core:build` fails identically
with the tree clean). The only change under `packages/**` is deleting
orphan markdown that no build or test reads. CI on this PR runs the real
lane.
## Not in this PR
Slack-side drafting/massaging in a dedicated channel, with write-back to
the release body. Deliberately separate — that lane needs its own
channel and webhook, and must not run through `#engr`. The `notify` job
and the `#engr` announcement are untouched here.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Release notes are now organized by release lane and recorded in
dedicated changelogs.
* GitHub Releases can automatically use the matching lane changelog
section.
* Release notes are scoped to packages included in each release lane.
* **Documentation**
* Added guidance for supported release lanes and changelog workflows.
* **Changes**
* Historical package and example changelog entries were removed or
replaced with the lane-based format.
* Notion-based release-note drafting and PR links are no longer used.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
One test drives 34 routes against a real lab server, so its cost is the
sum of 34 bounded waits and it lands wherever the runner's load puts it.
Measured across `test / unit` shards of the same commit: 29.2s on
Node 24 / React 19, 56.1s on Node 22 / React 18, 57.1s on
Node 20 / React 19, and, on two runs of one commit on Node 20 / React 18,
41.4s and then a timeout at the old 60s ceiling.
A five percent margin on the slowest shard is not a budget, so the new
ceiling is 180s, about three times the slowest passing run. The number
guards against a hang. It asserts nothing about elapsed time, because
this test measures no durations.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The notes now land in a source-controlled changelog instead of a scratch file
that rides the release branch. One file per lane, because the lanes version
independently: a shared file would interleave `1.70.0`, `angular/0.5.0` and
`channels/0.9.0` into one unreadable sequence.
monorepo -> CHANGELOG.md
angular -> packages/angular/CHANGELOG.md
channels -> packages/channels/CHANGELOG.md
`write-changelog.ts` prepends this release's section on the release branch,
create-pull-request commits it (a tracked file, always staged), and
`extract-release-notes.ts` reads the section back in the publish job as the
GitHub Release body. The changelog is therefore both the durable record and the
review surface: editing a section on the release PR changes what ships.
release-notes.md goes back to being ignored, so the same notes never exist as
two editable copies.
Also deletes 29 changesets-era changelogs that no tooling had written since
April. They stopped at 1.55.2 while the lane shipped 1.69.3, and
packages/angular/CHANGELOG.md still claimed 1.54.3 from before that lane split
onto its own 0.x line. Their content stays recoverable from git history. A test
pins the tracked changelog set to the lanes so they cannot creep back and
contradict the real versions.
Extraction never fails the publish job: it runs after npm publish, so a miss
annotates loudly and falls through to the existing bodyless-release fallback
rather than stranding the tag.
Committed with --no-verify: the pre-commit nx lane cannot run in this worktree
(packages/core and packages/channels-ui have no node_modules, and
`nx run @copilotkit/core:build` fails identically with the tree clean). The only
change under packages/** is deleting orphan markdown that no build or test
reads.
## Summary
- give locked Threads and Learning a shared conversion-focused layout
- add streamlined Loom demos, setup and engineer CTAs, and detailed
capability sections
- remove the disabled Threads preview so both locked experiences use the
same full-page treatment
## Why
Users without Intelligence enabled should immediately understand what
each feature provides and have clear paths to configure it or talk with
the team.
## How
- render both gates through one responsive locked-feature surface
- use minimal Loom embeds with feature-specific videos
- align content on a single centered rail with theme-aware styling
- add product-specific copy, capability icons, accessibility labels,
telemetry coverage, and fixture assertions
- verify with `pnpm nx run @copilotkit/web-inspector:test` and `pnpm nx
run @copilotkit/web-inspector:check-types`
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added unified locked views for Threads and Learning with feature
videos, capability outlines, setup guidance, and a “Talk to an Engineer”
call to action.
* Added responsive and dark-theme styling for locked-feature overviews.
* Updated self-hosted and setup messaging.
* **Bug Fixes**
* Prevented unavailable Threads content, examples, controls, and
metadata actions from appearing.
* Removed legacy runtime entitlement diagnostics from the interface.
* Standardized locked-state telemetry and CTA behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
#6818 renamed the Intelligence docs from `/premium/` to `/intelligence/`
and left the published packages alone. This cleans them up.
## The console notice
`@copilotkit/shared` printed this to every developer who used
`useCopilotChatHeadless_c` without a license key:
> To enable this **premium** feature, add your public license key…
> To learn more about **premium** features, read the documentation here:
docs.copilotkit.ai/**premium**/overview
"Premium" is not a tier we have. The notice now uses the sentence the
Headless UI docs page already uses — "Headless UI requires a CopilotKit
Intelligence license key" — so a developer who reads the notice and then
opens the docs finds the same words.
## One link was broken, not just stale
The "Show me how" button on the missing-public-API-key error opened
`/premium/overview#getting-access`. That heading was deleted on
2026-06-16 in 449237af0c, so the redirect carried the fragment to a page
that has no such anchor and the button landed at the top. It had been
doing that for two and a half months. It now points at
`#plans-and-access`, the section that answers how to get a key.
This was not in the ticket. It came out of tracing the links rather than
replacing them.
## The rest
Stale but working links move from `/premium/*` to `/intelligence/*` in
`react-core`, `web-inspector` and the runtime skill reference (two
byte-identical copies). Every one of them resolved through the redirects
from #6818; each cost a hop and named a retired tier. Four test files
assert these hrefs, so they move with the strings.
Not touched: `seo-redirects.ts` in `showcase/` keeps `/premium/`, since
those entries are what makes the old links work. `user_type: 'premium'`
in the provider examples is invented customer metadata, not a tier.
## Verification
- `nx run-many -t check-types,test` for `shared`, `react-core`,
`web-inspector`, `runtime`, `runtime-client-gql`: all green (17, 33,
152, 138 and 5 test files)
- On a cold worktree, `nx affected` first reported
`runtime-client-gql:check-types`, `runtime:check-types`, `runtime:test`
and `runtime:generate-graphql-schema` as failures. Cause: `check-types`
runs `tsc` concurrently with the `build` that generates
`src/graphql/@generated/*`, so the first run type-checks against files
that do not exist yet. Running `build` first makes all of them pass, and
Nx labels the same four as flaky. Unrelated to this change.
## Blocked on the docs promote
`docs.copilotkit.ai` has not been promoted to production since #6818
merged. Right now production serves `/premium/*` directly with a 200 and
returns 404 for every `/intelligence/*` path. Staging serves the new
paths with a 200 and redirects `/premium/overview` to
`/intelligence/overview` in one hop, so the code is right and the deploy
is pending.
CodeRabbit flagged this and it is correct. Promote `shell-docs` to
production before the next monorepo release, or these links 404 for
anyone who clicks them. Merging is safe on its own, because the packages
only reach users through a release.
## Companion change
The Intelligence repo carries the same rename for the CLI onboarding
prompt and the verify hint: CopilotKit/Intelligence#1117. The two ship
independently.
Refs OSS-1085
The Headless UI console notice told developers about "premium features" and
pointed at /premium/overview. The tier is called CopilotKit Intelligence now, so
the notice named a product that no longer exists. It now uses the same sentence
the Headless UI docs page uses.
The docs links in react-core, web-inspector and the runtime skill reference move
from /premium/* to /intelligence/*. They worked through the redirects added in
#6818, but each cost a hop and carried the old name.
One of them was broken, not just stale: the "Show me how" button on the missing
public API key error opened /premium/overview#getting-access. That heading was
deleted on 2026-06-16 in 449237af0c, so the button had been landing at the top
of the page for two and a half months. It now points at #plans-and-access, the
section that answers how to get a key.
Tests assert these hrefs, so they move with the strings.
Refs OSS-1085
Moves the published packages from 0.0.57 to the current AG-UI release across
@ag-ui/client, core, encoder and proto — 27 declarations in 18 packages.
0.0.59 is the first release carrying the subagent protocol surface
(SUBAGENT_STARTED/FINISHED/ERROR, subagentRunId) along with the null-omission
cleanup, so this is the dependency CopilotKit's subagent work needs.
Scope is packages/** plus the release script noted below. The examples and
showcases sit on a spread of older pins (0.0.40 through 0.0.58) and are left
alone.
One behavioural change comes with the bump. channels-core ships
sanitizeAgentEventStream because @ag-ui/client used to reject a TOOL_CALL_START
carrying parentMessageId: null — the shape @ag-ui/langgraph emits for an
interrupt-triggering tool call. 0.0.59 accepts that null and treats it as
absent, so the two tests asserting the run dies WITHOUT the sanitizer no longer
hold. They now assert the run survives, and the one at agent level still checks
the tool call actually arrives so it cannot pass vacuously. The sanitizer is
untouched and its coercion tests are unchanged; it is simply no longer the
thing keeping such a run alive.
The bump also broke the packed Angular consumer matrix. That job generates a
smoke app from scripts/release/lib/angular-package.ts, whose manifest restated
"@ag-ui/client": "0.0.57" as a literal while packages/angular moved to 0.0.59.
pnpm then installed both copies and the app failed to compile:
TS2322: Type 'SmokeAgent' is not assignable to type 'AbstractAgent'.
Types have separate declarations of a private property '_debug'.
The smoke app imports AbstractAgent directly, so it has to resolve the identical
copy the library ships against. Read that version off the packed manifest --
which verify-angular-package.ts already parses for the Angular support contract
-- instead of restating it, so no future AG-UI bump can desynchronise it.
Inspector Threads now has **Try from here**. One click copies a stored
thread into a Playground scratch session. The stored thread does not
change.
If the copy fails, Inspector stays on Threads and keeps the current
Playground scratch. Example tour threads and locked Threads do not show
the button.
## What does this PR do?
Adds **Try from here** on a real stored thread in Inspector Threads. One
click copies messages and thread state into a Playground scratch
session. The stored thread does not change.
If the copy fails, Inspector stays on Threads and keeps the current
Playground scratch. Example tour threads and locked Threads do not show
the button.
## Related PRs and Issues
- Linear: OSS-873
- Playground base: https://github.com/CopilotKit/CopilotKit/pull/6580
(merged)
## Checklist
- [x] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md)
- [x] If the PR changes or adds functionality, I have updated the
relevant documentation
- [x] "Allow edits by maintainers" is checked
## Testing
**Commands run**
1. Rebased `feat/oss-873-try-from-here` onto `origin/main` and resolved
6 conflict files.
2. `npx nx run @copilotkit/web-inspector:test` — 626 tests passed (after
the stale-result guard).
3. `npx nx run @copilotkit/web-inspector:check-types` — passed.
**Manual test**
1. Open Inspector on localhost with Intelligence on, so a real stored
thread exists.
2. Open that thread. Confirm **Try from here** is in the thread header.
3. Click **Try from here**. Confirm Inspector opens Playground with the
copied messages and the stored thread is unchanged.
4. Open an example tour thread. Confirm **Try from here** is not shown.
5. Force a copy failure (disconnect runtime). Confirm Inspector stays on
Threads and the prior Playground scratch is unchanged.
**How this PR makes testing easy**
- `packages/web-inspector/src/__tests__/inspector-navigation.spec.ts`
covers the button, copy path, failure path, and a stale click that must
not overwrite Playground.
- `packages/web-inspector/src/lib/__tests__/telemetry.test.ts` covers
`oss.inspector.threads_try_from_here_clicked`.
## Risk / rollback
Risk is limited to Inspector Threads and Playground. A revert of this PR
removes the button and the new telemetry event. No runtime protocol
change.
## Public API change
New Inspector telemetry export and event name:
**Before**
```ts
trackThreadsTabClicked(props);
```
**After**
```ts
trackThreadsTabClicked(props);
trackThreadsTryFromHereClicked({ ...props, outcome: "success" });
```
`CpkThreadInspector` also emits a `tryFromHere` custom event when the
user clicks the button.
Closes OSS-904.
## The problem
`CopilotKitCore.runtimeConnectionStatus` was set only by the `/info`
handshake, which runs **once on connect**. If the runtime became
unreachable after that, the status stayed `connected` indefinitely —
measured two independent ways against `examples/v2/react/demo` and
recorded in the ticket.
The failure was not lost, it was filed in the wrong drawer: it arrived
as `agent_run_failed`, indistinguishable from an agent bug. So
everything downstream inherited the wrong answer — System Health
reported healthy, the launcher error signal could not fire for the most
common real symptom ("it worked a minute ago"), and a customer `onError`
handler written to separate wiring problems from agent problems got the
wrong classification.
## What this changes
The status now reports **the outcome of the last actual contact with the
runtime**.
A failed runtime request — or silence past a per-request watchdog —
triggers **one** bounded confirmation request. If nothing answers, the
status moves to `error` and the failure is emitted through the existing
wiring error code, so customers already handling startup wiring failures
pick up the mid-session case without changing a line. A subsequent
successful request re-syncs and clears it.
Crucially, **the conversation survives**. The transition does not
discard runtime knowledge, so the agent backing an open chat is the same
instance, its messages stay on screen, and submitting stays possible —
which matters because submitting is what restores the status.
No polling, no heartbeat, no retry loop. Every timer is bound to one
request and dies with it.
## Decisions worth knowing when reviewing
- **Reactive in both directions.** A heartbeat would put permanent
background traffic into every embedding application; a retry loop mostly
races a user who is about to retry anyway. The cost is stated rather
than hidden: while nothing is happening, nothing is detected.
- **Status change is separated from discarding knowledge.** The only
pre-existing code that set `error` also cleared `remoteAgents`. That is
right at startup and destructive mid-session, because conversation state
lives on the agent instance. Four sites now hold this invariant up
together; each carries a comment saying so.
- **The trigger is deliberately permissive, and the check is the
arbiter.** A request that received a successful response never triggers
a check; user cancellation never does; everything else may. Defining the
trigger precisely would mean maintaining a status-code list that is
complete only for the deployment topologies someone thought of.
- **Silence counts.** A server can refuse (fails fast) or hang (accepts
and never answers). A stopped dev server refuses; a container
mid-rollout, a half-switched deploy and a dropped tunnel hang. Only
bounding the check does not help, because no check starts — hence the
per-request watchdog. It observes only and never cancels the request.
- **The rule is stated by destination, not by call site**, so a runtime
route added later inherits the behaviour. Excluded: the Intelligence
realtime endpoint (a different service — reporting its outage as
"runtime unreachable" would be a false diagnosis), endpoints belonging
to the customer, and the stop request.
- **Recovery may prune, under two conditions**: the runtime must have
reported at least one agent, and the agent must carry no conversation
state. An empty list is the signature of a runtime that has not finished
registering.
- **"Answered but refused" keeps the error status and gets a different
message.** An expired token means the app cannot work, so red is right;
telling the reader "unreachable" would send them to check ports and
containers.
## Deliberately not delivered
- Detecting an outage, or a recovery, while the application is idle.
- Recovery by opening the Threads view: every binding withholds thread
requests until the status is already connected, so nothing is sent while
it is red. The thread plumbing still earns its place for *detection*.
- A signal for the Intelligence realtime endpoint failing while the
runtime is healthy — a real gap, and its own ticket.
- Memory and suggestion routes adopting the instrumented fetch.
- A new status value or a new error code.
## Costs this introduces
`error` now means two things — "never connected, no agents" and "lost
mid-session, agents intact". Documented on the enum. And because the
status can now change mid-session at all, an outage costs some churn
that did not exist before: the memory list and the Inspector's thread
list are cleared and refetched, and where the chat owns its run-activity
store it is stopped and restarted. All of it is paid on a user-caused
transition, never while idle.
## Testing
Four independent reviewers audited an earlier revision of this branch;
the ten defects they reproduced are fixed and each is pinned by a test
that was red first. A mutation audit of 110 mutants killed 90; the
surviving holes were closed in the round after.
The connection-health suites carry 72 tests. Request counting is a
first-class assertion throughout, because several decisions are
*absences* — no polling, no retry loop, one check per burst, no traffic
while red — and an absence is only testable by counting. Those tests use
fake timers advancing ten minutes; that boundary is documented where it
lives, since anything slower is invisible to them.
Verified by hand in a browser with the runtime running as its own
process, so the page outlives it: a refusing runtime, a hanging runtime,
recovery, an agent added during an outage, an agent deleted during an
outage. `performance.timeOrigin` was checked throughout to prove the
page never reloaded and the result was not an artefact of a fresh
handshake.
## Follow-ups this leaves behind
Three of these deserve their own ticket. None blocks this PR; all three
are consequences of where its scope was drawn, and they are listed here
so the boundary is explicit rather than implied.
### 1. A signal for the Intelligence realtime endpoint
In Intelligence mode the browser gets its chat events from a **second
service** at its own address; the runtime is only asked for the
credentials. If that service fails while the runtime is healthy, this
change correctly reports the runtime as reachable — and the user
experiences exactly the silence this ticket exists to remove.
It is excluded here on purpose: folding it into the runtime status would
report "runtime unreachable" about a healthy runtime, and a false
diagnosis costs more debugging time than no signal. It needs its own
signal, which is a presentation decision as much as a detection one.
### 2. Memory routes onto the instrumented fetch
The memory store still builds with the global fetch, so its
runtime-bound requests are invisible to connection health. Two costs: a
genuine failure there is a signal we discard, and a success there cannot
restore the status.
The asymmetry is what makes this worth fixing rather than leaving:
memory is the surface most disrupted by a status transition (its list is
cleared and refetched) and currently the one least able to contribute.
The change itself is small — that module already takes its request
function as an injected dependency.
### 3. Consumers should key on what they need, not on the status value
Several consumers treat "status is not connected" as "discard
everything": the memory list, the Inspector's thread list, and the
chat's run-activity store. That was harmless while the status could not
change after page load. It can now, so every outage costs churn that did
not exist before.
This is the same mistake this PR fixes three times *inside* core — a
guard bound to a state instead of to the thing it protects. The
principle was applied internally and not to these consumers. That makes
the churn listed under "Costs" above **deferred rather than inherent**,
and it is the largest of the three follow-ups: three consumers in three
packages, each with its own risk, which is why it was kept out of this
PR.
### Two smaller items
- The launcher error signal on `main` carries a comment stating the
limitation this change removes ("a runtime that dies after the page
loaded … raises nothing … closing that gap means a re-probe in the
core"). It becomes false when this lands and should be corrected then.
- `packages/web-inspector/src/styles/generated.css` is build output
under version control and re-dirties the tree on every build. Unrelated
to this PR, but the Tailwind source glob scans test files, so any prose
comment containing a utility word (`fixed`, `hidden`, `visible`,
`block`) silently changes the committed CSS. Narrowing the glob would
remove the class of problem.
Full specification, including the interview decisions and every
revision: `OSS-904-PRD.md`.
## Why
The Inspector said what Intelligence *is* and linked out to a signup
page. Of ~1,655 Inspector opens in 90 days, **under 100 clicked any
CTA**. This replaces the feature list with an argument, and the outbound
link with an install that happens in the editor the developer is already
in.
This is the unfinished half of OSS-867, whose body asks for exactly
this: *"If a capability requires Intelligence, detail why and include a
video demonstrating that feature working end-to-end."*
## What changed
**A four-slide argument, paired to the picture beside it.** Each slide
carries two sentences and the visual they describe: your users' threads
→ the pattern inside them → the skill file → what it compounds into. An
earlier draft sold Threads in prose while animating Learning; bound
together they read as one chain, and `meeting-scheduling/SKILL.md`
recurs through all four so the closing diagram is checkable rather than
decorative.
Condensed from the six-phase animation on the Intelligence home page —
not screen-recorded. A ported version is themeable, stays sharp, and
costs no asset weight; the original also runs 21.4s and opens on the
agent booking the wrong meeting, a poor first frame for a card arguing
for the product.
**A copy-prompt button instead of a link out.** It hands the CLI's own
onboarding prompt to a coding agent. Every previous Intelligence CTA
opened a new tab into a signup form, which is where developers drop out.
It carries the CLI's `onboarding_run_id`, so
`oss.inspector.home_prompt_copied` can be joined to
`cli.onboarding.completed` on the Intelligence side. `home_cta_clicked`
only ever proved that someone clicked a link — this is the first event
that can show whether an install followed.
**Section anatomy mirrors System Health** (header band, rule, content),
so the action sits in the same top-right slot the status pill and renew
link already use, and the panel keeps one section shape throughout.
## Correctness of the claims
The copy was checked against the product's own surfaces, and two claims
did not survive:
- **Skills are not applied at run time.** Candidates land at
`pending_review`, a human approves, and the published set is pulled down
with `copilotkit skills download`. Nothing reads published skills during
a run. The slide says approve → pull in → the next run starts from what
worked.
- **Insights were missing**, and with them the evidence link that makes
Learning credible: every Insight cites the Threads and messages behind
it.
Also: *Rich Threads* is the product's name for the durable ones, and the
distinction is the whole sale next to a Threads tab full of local ones
that die on reload. "Your users" means the app's end users — which is
what the platform means too (`identifyUser` resolves one user per
request; a thread carries `end_user_id`, renamed from `user_id` because
the old name *"caused repeated misdiagnosis"*).
## Behaviour worth reviewing
- The story advances **only while Home is visible and the document is
not hidden**. A debugging tool should not hold a repeating timer behind
a closed panel.
- Slide motion is horizontal and derived from each slide's index
relative to the active one, so clicking a tab backwards animates
backwards with no stored direction to fall out of sync.
- Copied state **expires after 4s** so the button invites a second
press; a failed copy **does not**, because that state is the only place
the prompt is selectable by hand.
- Three modes, not two: a lapsed plan keeps the renew link and never
sees an install prompt.
- The rotating copy is hidden from assistive tech (it would announce
four times a loop); one stable sentence is exposed in its place and is
test-covered so it cannot quietly rot.
## Deliberate omissions
**No third-party coding-agent logos on the button**, unlike the
Intelligence app. That app is a private hosted surface; this package
ships inside other people's sites, and vendoring Anthropic's and
OpenAI's marks is not a call to make quietly. The helper line names the
agents in text.
Deferred and worth discussing separately: ordering the Home sections by
state (health first when something is broken, Intelligence first when
nothing is), and putting the same button in the locked Learning and
Threads tabs, where intent is highest.
## Verification
617 tests pass, `check-types` clean, oxlint 0 errors. Verified live in
both themes: all four slides, uniform 16px padding on every slide, card
height stable across slides, copy success **and** failure paths, and the
header band unchanged at 76px when the copied hint appears. The reset
behaviour is mutation-checked — the file records which mutation each
test does and does not catch.
Five packages publish to npm with no `license` field, so registry
metadata and automated license scanners report them as **Unknown**:
```
@copilotkit/agentcore-runner published=1.68.1 license=<NONE>
@copilotkit/core published=1.68.1 license=<NONE>
@copilotkit/sqlite-runner published=1.68.1 license=<NONE>
@copilotkit/voice published=1.68.1 license=<NONE>
@copilotkit/web-inspector published=1.68.1 license=<NONE>
```
The repo is MIT (see `LICENSE`) and every other published
`@copilotkit/*` package already declares it — these five were simply
missed. This adds `"license": "MIT"` to each, positioned before
`"repository"` to match the sibling packages.
## Why
Reported downstream in #2860, where a corporate procurement scan refused
packages whose license it could not resolve. That class of scanner reads
the `license` field from registry metadata; a `LICENSE` file in the repo
is not enough, and these packages ship no `LICENSE` file either.
**Correcting the record on that issue while I am here:** the `@ag-ui/*`
packages named in the original report are *not* affected. Every version
the reporter’s scanner flagged already carries `"license": "MIT"`:
```
@ag-ui/client@0.0.42 MIT
@ag-ui/core@0.0.37 MIT
@ag-ui/core@0.0.42 MIT
@ag-ui/encoder@0.0.42 MIT
@ag-ui/langgraph@0.0.20 MIT
@ag-ui/proto@0.0.42 MIT
```
`@ag-ui/core` has declared MIT since at least 0.0.35. An earlier triage
note on #2860 attributed the failure to a missing SPDX field upstream;
that was wrong, and why their scanner reported `Unknown` for `@ag-ui/*`
is still unexplained. This PR fixes the part that is genuinely defective
on our side.
## Testing
Metadata-only; no source, build, or runtime change.
- Confirmed the five missing fields against the live registry with `npm
view <pkg> license` (output above), and confirmed the other published
`@copilotkit/*` packages (`runtime`, `react-core`, `react-ui`, `shared`,
`sdk-js`, `angular`, `channels`, `channels-core`) already report `MIT`.
- Enumerated every non-private `packages/*/package.json` on
`origin/main` to confirm these five are the complete set missing the
field.
- Each edited file re-parsed with `json.load` and reports `MIT`.
- The `sync-lockfile` pre-commit hook resolved all 71 workspace projects
against the edited manifests without error.
Placement matches `packages/shared/package.json`, where `"license"`
immediately precedes `"repository"`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
The assertion searched footer.outerHTML for "241" to prove the unclamped
thread count never reaches the user. outerHTML also carries lit's part
markers, which lit builds as `lit$` + nine digits from Math.random(),
regenerated per process. Roughly one process in a hundred rolls a marker
containing those digits and fails the assertion with no relation to what the
footer rendered — this run drew lit$924125892$.
Comments are stripped before the check. Visible text and attributes still
count, so a genuine leak in an aria-label is caught exactly as before, and the
stripping is asserted so it cannot silently stop working.
Not introduced here: the same dice roll could hit any change to this package.
The other outerHTML assertions in the suite compare two strings from the same
process and share a marker, so they were never exposed.
The rail's four tabs reported nothing. Adds
oss.inspector.home_story_beat_selected, carrying the step as a property rather
than one event per label: the labels are expected to move as the story is
iterated, and per-label events would retire with them. beat_index rides along
so a reorder can be judged against where people actually click.
Only a press reports. The story also advances on its own every few seconds,
and reporting that would emit one event per idle developer per beat and bury
the handful of real interactions under a metronome — asserted, not assumed.
The checked-in artifact was produced mid-rebase, so its Tailwind token block
reflects the utilities in use at that point rather than on today's main. A
fresh build:css on the rebased tree differs; this is that output, so the
committed file matches what the build produces.
"What Intelligence does" promised an explanation and pointed at
intelligence.copilotkit.ai — the product and signup page. Mis-promising a
destination is a poor trade at the moment the card is asking to be trusted,
and the label was doing no work besides.
The two actions are two routes to one outcome: let the coding agent wire it
up, or go and do it in the browser. The secondary now says so — "Set it up
yourself" — which is honest about where it lands and quietly argues for the
primary, because "yourself" implies the other route is not.
The confirmation used to stand for the rest of the session. The reasoning was
that a developer leaves for their editor and comes back and should still find
the instruction — but whoever comes back has already pasted. The likelier
reason to return is to copy again, and a button wearing a checkmark and
"Prompt copied" reads as spent even though it still works.
Copied now reverts after 4s: button label and secondary line both return, so
the "What Intelligence does" link comes back too. Longer than the 2s the
Threads setup prompt uses, because this state carries an instruction to read
and not just an acknowledgement.
A failed copy does not expire. That state is the only place the prompt text is
selectable by hand, and pulling it away mid-selection would be worse than the
clipboard failing.
Three tests, mutation-checked: disabling the reset kills the two copied-state
tests. The failed-state test is blunter and only fails when both safeguards go
(the scheduling condition and the timer's own state re-check) — recorded in the
file so nobody reads more into it than it proves.
The instruction was a second row under the button, which pushed the action
column past the band's 76px min-height and shoved the whole story down at the
moment the developer had just acted — the worst possible time for the layout
to move.
It now takes over the secondary slot instead of adding to it. Before the press
the useful aside is "what is this"; after it, "where to put it". Both are
single lines in the same flex slot, so the swap cannot change the height:
measured 76px and an unmoved story in both states.
Dropping the line was the alternative, but it is the one instruction that
cannot go: a prompt on the clipboard with no idea what to do with it converts
nobody. It is also vendor-neutral now — naming three editors was brittle and
the prompt identifies the agent itself.