mirror of
https://github.com/CopilotKit/CopilotKit.git
synced 2026-09-14 16:26:20 +08:00
fix/fac-49-intelligence-db-init
13613 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3a4118321d | QA Factory Coding update for FAC-49 | ||
|
|
599f6e4930 | QA Factory Coding update for FAC-49 | ||
|
|
9012598bc8 | QA Factory Coding update for FAC-49 | ||
|
|
5144fc7111 | Merge remote-tracking branch 'refs/remotes/origin/main' into fix/fac-49-intelligence-db-init | ||
|
|
033b0ae25a |
Make Angular docs a first-class docs surface (#6138)
## Summary Angular is now a first-class docs surface instead of a small frontend island: - give `/angular` the full shared documentation information architecture, including deployment, troubleshooting, What's New, Channels, CLI, telemetry, backend setup, Runtime, agentic protocols, and CopilotKit Enterprise Intelligence - keep frontend and backend selection orthogonal, preserving the selected backend when switching React ↔ Angular - share frontend-independent content once and swap only frontend-owned sections, avoiding an Angular × backend × topic copy matrix - resolve React-owned topics to Angular-native task guides and canonical redirects instead of showing React hooks or provider setup - render Angular-specific branches correctly in pages and `.mdx`/LLM output, including nested shared snippets - source Angular examples from runnable Showcase regions for tools, agent config, sub-agent delegation, headless UI, interrupts, authentication, and Mastra activity renderers - add Angular-native shared-state, HITL, generative UI, auth, thread, troubleshooting, metadata, canonical, search, and sitemap coverage - add a complete `injectThreads` API reference so the threads journey has no dangling handoff ## Guardrails - exact React → Angular information-architecture parity test across the root and all 20 visible backends; every React destination maps to a published Angular destination - exhaustive Angular × every visible backend render test: every page resolves, and rendered Markdown contains no React package, hook, provider, or unexpanded frontend/snippet components - shared Runtime, Intelligence, deployment, troubleshooting, Channels, and operational routes stay canonical instead of being duplicated for every backend - backend-owned pages appear under a backend only when that backend genuinely changes the content - frontend selector links preserve backend context - Showcase source-region tests ensure Angular snippets remain tied to runnable code - shell-docs image contract ensures Angular source is staged and bundled before the production build ## Validation - `npm run test` in `showcase/shell-docs` — 42 files / 209 tests - `npm run typecheck` in `showcase/shell-docs` - `npm run lint` in `showcase/shell-docs` — clean apart from existing warnings - `npm run build` in `showcase/shell-docs` — 222 static pages - `pnpm nx run @copilotkit/showcase-scripts:test --skip-nx-cache` — 72 files / 2,324 tests - `pnpm nx run @copilotkit/showcase-angular-host:typecheck --skip-nx-cache` - clean root-context `showcase/shell-docs/Dockerfile` build — all content generators, TypeScript, and 222 static pages - production-server smoke checks for Angular root, deployment, troubleshooting, What's New, Channels, telemetry, LangGraph agent config/sub-agents, API reference, `.mdx` output, canonical redirects, and sitemap - rendered `.mdx` audit confirms shared backend guidance and Showcase-derived Angular snippets are present with no React imports/hooks or raw conditional tags The broad `verify-shell-docs:fast` audit remains red on the repository's existing whole-tree dead-link/content-shape baseline; the same audit is red on `main`. Branch-specific tests and production routes are green. Draft intentionally; mark ready only when requested. |
||
|
|
dacc667355 | fix(docs): resolve Angular overview guide links | ||
|
|
852990ccd1 | fix(docs): select Angular quickstart branch before MDX | ||
|
|
f43682b3db | test(showcase): align Angular staging assertion | ||
|
|
b7e83d7c1f | docs: make Angular journeys capability-aware | ||
|
|
8a2844b571 | docs: enforce angular-only documentation routes | ||
|
|
9988c23890 | docs: render shared concepts for Angular | ||
|
|
cc8689e21f | fix(showcase): bundle Angular docs source in image | ||
|
|
a87f1c9a30 | docs: close Angular information architecture gaps | ||
|
|
db732d3966 | docs: complete Angular threads API journey | ||
|
|
38333d3625 | docs: enforce Angular parity across backend guides | ||
|
|
92e5c31332 | docs: align Angular metadata with rendered variants | ||
|
|
fcf2357c25 | docs: add Angular-native content and source-backed snippets | ||
|
|
c569aa595d | docs: make Angular share the full information architecture | ||
|
|
47ce2720a6 |
fix(release): make verify:runtime-package pass on channels release PRs (#6186)
Fixes OSS-616 ## Problem `scripts/release/verify-runtime-package.ts` **cannot pass on a channels release PR**, so the `unit (20.x)` leg is red by construction on exactly the PRs the gate exists to protect — and gets merged past. 1. `packages/runtime/package.json` declares `@copilotkit/channels-intelligence: workspace:*`. 2. `pnpm pack` rewrites `workspace:` to the workspace's *current* version — on a release PR, the freshly-bumped one (e.g. `0.3.0`). 3. The temp consumer then has to resolve `@copilotkit/channels-intelligence@0.3.0` **from npm**. 4. That version doesn't exist yet. This PR is what publishes it. Real CI output from #6185: ``` ERR_PNPM_NO_MATCHING_VERSION No matching version found for @copilotkit/channels-intelligence@0.3.0 while fetching it from https://registry.npmjs.org/ This error happened while installing the dependencies of @copilotkit/runtime@1.63.2 The latest release of @copilotkit/channels-intelligence is "0.2.1". Published at 7/17/2026 ``` Red instances: **#6185** (`channels/v0.3.0`) and **#6025** (`channels/v0.2.1`, merged red on 2026-07-17). #5989 (`v0.2.0`) passed only because the hard runtime dependency landed the next day in `fea464de52`. ## Fix `verify-channels-umbrella.ts` already solved this — its own comment describes the problem verbatim. Same approach applied here: pack the whole first-party `workspace:` closure locally and pin it through pnpm `overrides` so nothing unpublished is resolved over the network. The packing helpers (`packPackage`, the transitive `workspace:` walk) move into `scripts/release/lib/pack-workspace.ts`, shared by both scripts instead of duplicated. `verify-channels-umbrella` behavior is unchanged — it just calls the extracted helpers. **The check is not weakened.** It still asserts the packed runtime declares `@copilotkit/channels-intelligence` as a real dependency and that the package loads through both ESM and CJS. A new guard fails loudly if channels-intelligence ever stops being a `workspace:` dep, so a silent fallback to registry resolution can't creep back in. Runtime dependency ranges were not touched. ## Testing All runs under Node 20 in a clean worktree off `origin/main`. **1. Reproduced the failure** — applied the exact 9 version bumps from `release/publish/channels/v0.3.0`, then ran the pre-fix script from `origin/main`: ``` The latest release of @copilotkit/channels-core is "0.2.1". Published at 7/17/2026 Command failed: pnpm install --ignore-scripts ``` **2. Fixed script, same bumped state** — acceptance criterion 1: ``` $ pnpm run verify:runtime-package OK: packed runtime installs @copilotkit/channels-intelligence and loads through ESM and CJS. EXIT=0 ``` **3. Still fails when the dependency is removed** — deleted `@copilotkit/channels-intelligence` from `packages/runtime/package.json`: ``` packed runtime must install @copilotkit/channels-intelligence as a dependency EXIT=1 ``` **4. Still fails on a genuine ESM load break** — `throw` at the top of `packages/channels-intelligence/src/index.ts`: ``` Error: simulated ESM load break Command failed: pnpm exec node --experimental-import-meta-resolve --input-type=module --eval ... EXIT=1 ``` **5. Still fails on a genuine CJS load break** — `throw` at the top of `packages/runtime/src/v2/index.ts`: ``` Error: simulated CJS load break Command failed: pnpm exec node --eval require("@copilotkit/runtime"); EXIT=1 ``` **6. Umbrella check unaffected by the helper extraction:** ``` $ pnpm run verify:channels-umbrella OK: local Channels snapshot is exact, compatible, singly resolved, and TSX-consumable. ``` **7. Release script tests** — new `scripts/release/__tests__/verify-runtime-package.test.ts` models the release-PR case (bumped local versions absent from the registry) and asserts no workspace dependency is left registry-resolvable. The vitest include glob was extended to cover `__tests__/`: ``` Test Files 10 passed (10) Tests 137 passed (137) ``` **8.** `oxfmt --check` clean, `oxlint` 0 warnings / 0 errors, `tsc --strict --noEmit` clean on all four touched scripts. All mutations from steps 1 and 3–5 were reverted; the final diff touches `scripts/release/` only. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
4a9837e176 |
fix(release): pack runtime workspace deps locally in verify-runtime-package
`verify:runtime-package` could not pass on a channels release PR. `pnpm pack` rewrites the runtime's `workspace:` ranges to the workspace's current version, so on a release PR the temp consumer tried to resolve the freshly-bumped `@copilotkit/channels-intelligence` from npm — the version this very PR is about to publish. It failed `unit (20.x)` by construction on #6185 and #6025. Apply the fix `verify-channels-umbrella` already uses: pack the whole first-party `workspace:` closure locally and pin it through pnpm `overrides`. The packing helpers move to `lib/pack-workspace.ts` so both scripts share one implementation instead of duplicating it. The contract is unchanged: the packed runtime must still declare channels-intelligence as a real dependency, and it must still load through both ESM and CJS. |
||
|
|
8180a9160d |
chore: release channels v0.3.0 (#6185)
## Release channels v0.3.0 **Scope:** `channels` | **Bump:** `minor` --- ### How this release process works 1. **This PR was created automatically** by the "release / create-pr" workflow. It bumped the `channels` packages to `0.3.0` and generated AI-enhanced release notes. 2. **CI runs on this PR** — the full test suite (unit tests, lint, type checks, build) must pass before merging. This is the review gate. 3. **Review the release notes** in `release-notes.md` in this PR. If a Notion draft was created, you can edit the release notes there before merging. 4. **When this PR is merged**, the `release / publish` workflow automatically: - Builds all packages - Publishes the `channels` packages to npm at version `0.3.0` - Creates git tag `channels/v0.3.0` - Creates a GitHub Release with the final release notes ### Before merging - [ ] CI is green (tests, lint, types, build) - [ ] Version bumps look correct - [ ] Release notes are accurate (edit in Notion if a draft was created) --- > **Do not merge until CI is fully green.** The full test suite runs automatically on this PR.channels/v0.3.0 |
||
|
|
0e19eb2d5d | chore: release channels v0.3.0 | ||
|
|
13ee9f5b99 |
Revise Angular quickstart link in README (#6127)
Updated the Angular section to include 'Source Code & Quickstart' instead of 'Source Code - Quickstart coming soon'. <!-- Thank you for sending the PR! We appreciate you spending the time to work on these changes. Help us understand your motivation by explaining why you decided to make this change. **Please PLEASE reach out to us first before starting any significant work on new or existing features.** By the time you've gotten here, you're looking at creating a pull request so hopefully we're not too late. We love community contributions! That said, we want to make sure we're all on the same page before you start. Investing a lot of time and effort just to find out it doesn't align with the upstream project feels awful, and we don't want that to happen. It also helps to make sure the work you're planning isn't already in progress. As described in our contributing guide, please file an issue first: https://github.com/ag-ui-protocol/ag-ui/issues Or, reach out to us on Discord: https://discord.com/invite/6dffbvGU3D You can learn more about contributing to copilotkit here: https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md Happy contributing! --> ## What does this PR do? (Describe the changes introduced in this PR) ## Related PRs and Issues - (Direct link to related PR or issue, if relevant) ## Checklist - [ ] I have read the [Contribution Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md) - [ ] If the PR changes or adds functionality, I have updated the relevant documentation - [ ] "Allow edits by maintainers" is checked (lets us help iterate on your PR directly — faster turnaround for everyone) |
||
|
|
4efb0969c0 |
chore(deps): update jarvusinnovations/background-action action to v2 (#6181)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [JarvusInnovations/background-action](https://redirect.github.com/JarvusInnovations/background-action) | action | major | `v1` → `v2` | # Warnings (1) Please correct - or verify that you can safely ignore - these warnings before you merge this PR. - `JarvusInnovations/background-action`: Could not determine new digest for update (github-tags package JarvusInnovations/background-action) --- --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/592) for more information. --- ### Release Notes <details> <summary>JarvusInnovations/background-action (JarvusInnovations/background-action)</summary> ### [`v2.0.0`](https://redirect.github.com/JarvusInnovations/background-action/releases/tag/v2.0.0) [Compare Source](https://redirect.github.com/JarvusInnovations/background-action/compare/v2.0.0...v2.0.0) #### Breaking changes - **Invalid option values now fail the step.** `tail`, `log-output`, `log-output-resume` and `log-output-if` used to match on substrings, so `log-output: no-stderr` quietly *enabled* stderr and a typo like `sdtout` quietly disabled logging entirely. The same applied to `wait-for`, where `abc123` silently became a 123ms timeout. These are now validated exactly. **If a workflow of yours currently passes with a typo'd value, it will start failing — that value was never doing what it looked like.** - **Backgrounded processes are stopped during post-run.** They receive `SIGTERM`, get `shutdown-grace` (default `10s`) to exit cleanly, and anything they print on the way down is captured in the logs. Set `shutdown: false` for the previous behavior. - **Logs are written under `RUNNER_TEMP`** instead of the workspace, so an automated commit can no longer sweep them into your repository. Use the new `stdout-log` / `stderr-log` outputs to find them. - **Runs on the `node24` runtime.** #### Fixes - **`wait` was passed as an argument to your last command** ([#​210](https://redirect.github.com/JarvusInnovations/background-action/issues/210)). `core.getInput()` strips trailing whitespace, so the shell builtin appended to your commands became an argv of whatever ran last — `npm run preview` became `npm run preview wait`. Affected every multi-line `run:` whose final command wasn't backgrounded. - **A failed background process is now reported immediately** rather than at the readiness timeout. A bare `wait` blocks until *every* job exits and discards their statuses, so one service dying on startup was invisible until the timeout this action exists to prevent. Jobs are now awaited one at a time. - **Backgrounded processes are no longer left running** ([#​205](https://redirect.github.com/JarvusInnovations/background-action/issues/205)). - **Log files no longer land in the workspace** ([#​199](https://redirect.github.com/JarvusInnovations/background-action/issues/199)), and their paths are published as outputs ([#​193](https://redirect.github.com/JarvusInnovations/background-action/issues/193)). - **`log-output-if: early-exit`** — the spelling `action.yml` had advertised since the first release — was rejected outright. Both spellings now work. - **Post-run no longer prints the environment** when debug logging is enabled. #### Improvements - New inputs: `shutdown`, `shutdown-grace`. New outputs: `stdout-log`, `stderr-log`. - Durations accept terse and verbose spellings (`30s`, `30 seconds`, `1h30m45s`). - Production dependencies reduced to three, with **no known advisories**. - The published bundle is now committed and verified against its sources on every pull request. - Tests run on macOS as well as Linux. See the README for the full upgrade notes. #### Thanks This release is almost entirely other people's bug reports. Several sat open for a long time. - **[@​OlegYch](https://redirect.github.com/OlegYch)** reported [#​210](https://redirect.github.com/JarvusInnovations/background-action/issues/210), and pointed at the exact line in `index.js`. That's most of the diagnosis. - **[@​rdicroce](https://redirect.github.com/rdicroce)** reported [#​199](https://redirect.github.com/JarvusInnovations/background-action/issues/199) after log files were accidentally committed to a repository by an automated release workflow. - **[@​piranna](https://redirect.github.com/piranna)** reported [#​205](https://redirect.github.com/JarvusInnovations/background-action/issues/205), correctly noting nothing ever killed the backgrounded processes. - **[@​vera](https://redirect.github.com/vera)** reported [#​187](https://redirect.github.com/JarvusInnovations/background-action/issues/187), the post-job `ENOENT` failure. Now covered by a regression test. - **[@​JordanLongstaff](https://redirect.github.com/JordanLongstaff)** reported [#​206](https://redirect.github.com/JarvusInnovations/background-action/issues/206) (Node 20 deprecation), and **[@​marcodicro-dp](https://redirect.github.com/marcodicro-dp)** for following up on it. - **[@​riderx](https://redirect.github.com/riderx)** ([#​207](https://redirect.github.com/JarvusInnovations/background-action/issues/207)), **[@​runlevel5](https://redirect.github.com/runlevel5)** ([#​208](https://redirect.github.com/JarvusInnovations/background-action/issues/208), [#​209](https://redirect.github.com/JarvusInnovations/background-action/issues/209)) and **[@​Lucas127128](https://redirect.github.com/Lucas127128)** ([#​211](https://redirect.github.com/JarvusInnovations/background-action/issues/211)) all sent Node 24 upgrades. [#​207](https://redirect.github.com/JarvusInnovations/background-action/issues/207) was the closest to complete, and [#​209](https://redirect.github.com/JarvusInnovations/background-action/issues/209)'s ESM work remains valuable and is still on the table. Sorry it took a while. ### [`v2`](https://redirect.github.com/JarvusInnovations/background-action/compare/v1.0.7...v2.0.0) [Compare Source](https://redirect.github.com/JarvusInnovations/background-action/compare/v1.0.7...v2.0.0) </details> --- ### Configuration 📅 **Schedule**: (in timezone America/Los_Angeles) - Branch creation - "before 9am every weekday" - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/CopilotKit/CopilotKit). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yODAuMCIsInVwZGF0ZWRJblZlciI6IjQzLjI4MC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119--> |
||
|
|
b6e0bc0cd2 | chore(deps): update jarvusinnovations/background-action action to v2 | ||
|
|
3274398f72 |
fix(showcase/ms-agent-dotnet): resolve OPENAI_API_KEY-first via ApiKeyResolver (#6175)
## What Ports `ms-agent-harness-dotnet`'s `ApiKeyResolver` into `ms-agent-dotnet` so its 15 agent factories resolve the OpenAI credential as `OPENAI_API_KEY` (env) → `config[OPENAI_API_KEY]` → `GitHubToken`, and the endpoint via `OPENAI_BASE_URL` → default — instead of hardcoding `configuration["GitHubToken"]` in each agent. ## Why `ms-agent-dotnet`'s main chat clients authenticated **only** with the GitHub token. On a fixture-miss fall-through, aimock proxies to real `api.openai.com`, which rejects a GitHub token (`invalid_api_key`, surfaced as HTTP 502) — the ms-agent-dotnet staging outage. `ms-agent-harness-dotnet` already works because it resolves `OPENAI_API_KEY` first; this brings `ms-agent-dotnet` to parity. Interim showcase-side mitigation while the aimock cross-provider guard (**PNI-108** / CopilotKit/aimock#340) is deferred. See also PNI-109 (inject `x-aimock-context`) and PNI-110 (per-request upstream routing). ## Changes - Copy `agent/ApiKeyResolver.cs` verbatim from the harness column (code copy, no new dependency) - Rewire 15 factories + `A2uiSecondaryToolCaller` → `ResolveApiKey` / `ResolveEndpoint`; drop each file's now-dead `DefaultOpenAiEndpoint` const (−162/+47) - Add `tests/ApiKeyResolverTests.cs` — 11 cases (precedence, env-over-config, whitespace-skip, mock-endpoint fallback, non-mock fail-fast) ## Verification - `dotnet build` → **0 errors / 0 warnings** - `dotnet test` → **76/76** (incl. 11 new resolver tests) - `dotnet format whitespace --verify-no-changes` → clean - **Live**: built the client exactly as the agents do, resolved a real `OPENAI_API_KEY`, called `api.openai.com` → **200** (confirms the 502 path is fixed). Throwaway smoke, not committed. ## ⚠️ Operational follow-up (required to flip staging/prod) `showcase-ms-agent-dotnet` must have a valid **`OPENAI_API_KEY`** set in Railway (as `ms-agent-harness-dotnet` already does). This code enables the behavior; the env var makes it live. ## Not run here Showcase Docker value-test (`bin/showcase test ms-agent-dotnet:… --d6`) — needs the container stack; worth running before merge. |
||
|
|
a3ad8960c9 | Merge branch 'main' into mark/ms-agent-dotnet-apikeyresolver | ||
|
|
e9148b3050 |
fix(sdk-python): bridge copilotkit context into LangGraph subgraphs (#5373)
## Summary `CopilotKitMiddleware` loses frontend actions and app context when a middleware-wrapped agent runs as a LangGraph subgraph. Parent graphs propagate run context to child nodes, but `state["copilotkit"]` stays empty there, so the child misses both frontend-tool injection and the `App Context:` note. ## Root cause `LangGraphAGUIAgent.langgraph_default_merge_state(...)` materializes CopilotKit data into state for top-level runs, but child LangGraph nodes inherit runtime context rather than arbitrary parent state. The missing piece was earlier in the write path: by the time the base agent calls `get_stream_kwargs(...)`, it has already replaced the original AG-UI request with merged graph state, so serializing from that argument drops the frontend tools and app context on the real subgraph path. The fix moves ownership to the run entry: `LangGraphAGUIAgent.run(...)` now captures the frontend tools and app context from the real `RunAgentInput`, and `get_stream_kwargs(...)` forwards that captured payload into LangGraph using `context["copilotkit"]` when the graph supports runtime context, or `config["configurable"]["copilotkit"]` on older graph shapes. `CopilotKitMiddleware` still uses one shared lookup order everywhere it reads CopilotKit data: `state["copilotkit"]`, then `runtime.context["copilotkit"]`, then LangGraph config fallbacks. That same carrier now drives the app-context-note helper path too. ## Changes - `sdk-python/copilotkit/langgraph_agui_agent.py`: capture frontend actions and app context from the original `RunAgentInput` in `run(...)`, then write the captured payload into the LangGraph runtime carrier that the current graph shape supports before the subgraph starts. - `sdk-python/copilotkit/copilotkit_lg_middleware.py`: unify frontend-tool, app-context, and interception lookups around the same carrier order, and cover the current-`main` app-context helper path. - `sdk-python/tests/test_agui_agent.py`: add focused coverage for both the runtime-context path and the configurable fallback path on graphs without `context` support. - `sdk-python/tests/test_copilotkit_lg_middleware.py`: replace the manually seeded subgraph proof with a real `LangGraphAGUIAgent.run(...)` regression that reaches the child middleware through the production path. ## What is NOT changed - Top-level agents still prefer `state["copilotkit"]`; the runtime bridge only fills the subgraph gap. - Header propagation behavior is unchanged. - No JS LangGraph executor changes are part of this PR. ## Related PRs and Issues Closes #3886. ## Test plan - [x] `cd sdk-python && pytest tests/test_agui_agent.py tests/test_copilotkit_lg_middleware.py -q` - 108/108 pass. Covers the AGUI run-entry bridge, the configurable fallback for graphs without `context` support, frontend-tool injection, app-context note injection, and a real `create_agent` subgraph invocation through `LangGraphAGUIAgent.run(...)`. - [x] `ruff check sdk-python/copilotkit/copilotkit_lg_middleware.py sdk-python/copilotkit/langgraph_agui_agent.py sdk-python/tests/test_agui_agent.py sdk-python/tests/test_copilotkit_lg_middleware.py` - pass. Covers Python lint on every touched file. - [x] `ruff format --check sdk-python/copilotkit/copilotkit_lg_middleware.py sdk-python/copilotkit/langgraph_agui_agent.py sdk-python/tests/test_agui_agent.py sdk-python/tests/test_copilotkit_lg_middleware.py` - pass. Covers repo formatting on every touched file. - [ ] CI green (`static / quality`, `test / unit` on Node 20/22/24) |
||
|
|
3ff7a51606 |
fix(showcase): pin ag2<1.0.0 to unblock CI after upstream module rename (#6176)
## What's broken
`ag2` **1.0.0 was published to PyPI at 2026-07-27T00:58:37Z**. It
renames the top-level module from `autogen` to `ag2` and ships **no
`autogen/` package at all**.
`showcase/integrations/ag2/requirements.txt` floated on
`ag2[openai,ag-ui]>=0.9.0`, so every *fresh* CI dependency resolution
now pulls 1.0.0 and pytest collection dies on import.
This breaks `Python unit tests (3.10)` and `Python unit tests (3.12)` in
`showcase_validate.yml` **on `main` and on every open PR**. It's
time-based, not commit-based — `main` currently reads green only because
it hasn't re-run since the release landed.
### Release timing evidence
From the PyPI JSON API:
| version | upload_time_iso_8601 |
|---|---|
| 0.14.0 | `2026-06-26T04:48:39.784002Z` |
| 1.0.0 | `2026-07-27T00:58:37.996353Z` |
### Wheel layout proof
Top-level directories in each wheel:
```
=== ag2 0.14.0 === === ag2 1.0.0 ===
ag2-0.14.0.dist-info ag2
autogen ag2-1.0.0.dist-info
templates
```
`autogen/` is simply gone in 1.0.0.
## Why pin instead of migrating the imports
**Because 1.0.0 is not a rename — it's an API change.** Diffing the
wheels:
1. `autogen/ag_ui/adapter.py` (the stable, `AgentService`-based
`AGUIStream`) is **deleted**. `ag2/ag_ui/` in 1.0.0 is the *promoted
former `autogen.beta.ag_ui`* — a different implementation with
`events.py`/`stream.py`, matching old `autogen/beta/ag_ui/`, not old
`autogen/ag_ui/`.
2. The constructor lost a parameter:
```python
# 0.14.0 autogen/ag_ui/adapter.py
def __init__(self, agent, *, event_interceptors=()) -> None:
# 1.0.0 ag2/ag_ui/stream.py
def __init__(self, agent: Agent) -> None:
```
3. `dispatch()` changed shape — `context=` is **gone**, replaced by
`variables=` plus new
`prompt`/`dependencies`/`config`/`tools`/`middleware`/`observers`/`hitl_hook`
kwargs.
`showcase/integrations/ag2/src/agents/_multimodal_normalize.py:330`
subclasses `AGUIStream` and calls:
```python
async for chunk in super().dispatch(incoming, context=context, accept=accept):
```
So a naive import-path rewrite would trade a loud `ModuleNotFoundError`
at import time for a **silent-until-hit `TypeError` at request time** —
strictly worse while CI is on fire. A genuine 1.0 migration has to port
~20 `autogen` import sites (including
`autogen.agentchat.ContextVariables`, `autogen.tools.tool`,
`autogen.code_utils.content_str`) and re-verify every ag2 demo cell
behaviorally. That's real work, not an unblock.
## Coverage — every `ag2` dependency site
Swept all `requirements*.txt`, `*.lock`, `uv.lock`, `poetry.lock`, and
`pyproject.toml` in the repo:
```
./showcase/integrations/ag2/requirements.txt:1:ag2[openai,ag-ui]>=0.9.0
```
**Exactly one** declaration — no lockfile pins it, no other integration
depends on it, transitively or otherwise. All `autogen` imports live
under `showcase/integrations/ag2/`. This one-line bound closes the whole
surface; nothing else is left floating.
## Red-green proof
Both runs use a clean `python3.12 -m venv` + `pip install -r
requirements.txt`, i.e. a fresh resolution exactly like CI's.
### RED — before the fix (resolves ag2 1.0.0)
```
$ pip show ag2 | head -2
Name: ag2
Version: 1.0.0
$ python -m pytest tests/python/ -q
==================== ERRORS ====================
________ ERROR collecting tests/python/test_multimodal_normalize.py ________
ImportError while importing test module '.../tests/python/test_multimodal_normalize.py'.
Traceback:
tests/python/test_multimodal_normalize.py:77: in <module>
from agents._multimodal_normalize import ( # noqa: E402
src/agents/_multimodal_normalize.py:75: in <module>
from autogen.ag_ui import AGUIStream, RunAgentInput
E ModuleNotFoundError: No module named 'autogen'
=============== short test summary info ===============
ERROR tests/python/test_multimodal_normalize.py
!!!!!! Interrupted: 1 error during collection !!!!!!
1 warning, 1 error in 0.30s
```
### GREEN — after the fix (resolves ag2 0.14.0)
```
$ pip show ag2 | head -2
Name: ag2
Version: 0.14.0
$ python -m pytest tests/python/ -q
...................... [100%]
=============== warnings summary ===============
tests/python/test_cvdiag_boundaries.py:34
StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated
22 passed, 1 warning in 1.52s
```
## Follow-up left undone
Pinning defers the migration; it doesn't cancel it. What remains:
- **Port to ag2 1.x.** Rewrite ~20 `autogen.*` imports to `ag2.*`,
replace the deleted adapter-based `AGUIStream` with the new stream API,
drop/replace `event_interceptors` in `_multimodal_normalize.py`, and
convert `dispatch(context=...)` to `dispatch(variables=...)`. Then
behaviorally re-verify every ag2 demo cell — the beta stream promoted
into `ag2.ag_ui` is a different implementation, so cell-level parity
cannot be assumed from a green unit suite.
- **Renovate** will eventually propose lifting the `<1.0.0` bound. That
PR must not be merged as a routine bump; it's the migration above.
- The inline comment in `requirements.txt` records all of this at the
point of change so the bound isn't lifted casually.
## Second commit: `validate-pins` ratchet hash
The `Validate Showcase` job's `validate-pins (ratchet)` step failed on
the first push. Cause is benign and expected: `ag2` matches
`FRAMEWORK_PATTERNS`, which demands an exact `==` pin, so the dep was
**already** in the drift baseline as a `[FAIL]`. Adding `,<1.0.0`
changes that line's text, which changes the SHA-256 the ratchet takes
over the sorted FAIL set.
Verified the FAIL *set* is otherwise untouched — I reproduced the
committed baseline hash on a pristine `origin/main` tree, then diffed:
```
$ diff <(pristine FAIL lines) <(fixed FAIL lines)
[FAIL] ag2: ag-ui-protocol is not an exact pin (>=0.1.10)
-[FAIL] ag2: ag2 is not an exact pin (>=0.9.0)
+[FAIL] ag2: ag2 is not an exact pin (>=0.9.0,<1.0.0)
[FAIL] ag2: openai is not an exact pin (>=1.50.0)
```
Exactly one line differs. `Summary: OK=5 SKIP=0 WARN=3 FAIL=31` both
before and after — **count unchanged at 31**, nothing healed, nothing
regressed. So this is a hash-only baseline update and the "never raise
the count without sign-off" invariant is not engaged.
Pristine run reproduced `b47ca987…` (identical to the committed
baseline), confirming the validator is deterministic and this diff is
the sole delta.
### Why a range bound rather than an exact `==0.14.0`
An exact pin would satisfy `validate-pins` outright and ratchet the
count 31 → 30, but I kept the range deliberately:
- `0.14.0` is the highest final pre-1.0 release and the 0.x line is now
legacy, so the range resolves to exactly `0.14.0` today — proven by the
GREEN run above.
- `1.0.0b0` sorts below `1.0.0` under PEP 440, but pip excludes
pre-releases when a final release satisfies the specifier, so the bound
cannot pull the beta. Empirically confirmed: the GREEN venv resolved
`0.14.0`.
- It leaves room for a future `0.14.x` patch or security release without
another PR.
- It matches the sibling deps in the same file (`ag-ui-protocol>=`,
`openai>=`) and keeps this urgent fix to a pure hash bump rather than a
count ratchet.
Tightening to an exact pin (and ratcheting the count down) is reasonable
follow-up if the team wants to drive `validate-pins` drift toward zero,
but it's a pin-discipline cleanup, not part of this unblock.
|
||
|
|
4947c4a785 |
fix(showcase): update validate-pins hash for the ag2 bound
The `<1.0.0` upper bound changes the text of an existing `[FAIL]` line
from `ag2 is not an exact pin (>=0.9.0)` to `(>=0.9.0,<1.0.0)`, which
shifts the ratchet's SHA-256 over the sorted FAIL set.
The FAIL *set* is otherwise identical -- count stays 31, nothing healed
and nothing regressed. Verified by reproducing the committed baseline
hash b47ca987 on a pristine origin/main tree, then diffing the FAIL
lines against the fixed tree; exactly one line differs:
-[FAIL] ag2: ag2 is not an exact pin (>=0.9.0)
+[FAIL] ag2: ag2 is not an exact pin (>=0.9.0,<1.0.0)
`ag2` matches FRAMEWORK_PATTERNS, which demands an exact `==` pin, so
the dep was already in the baseline's drift set before this change and
remains in it after. Hash-only update; the count is untouched, so the
"never raise the count without sign-off" invariant is not engaged.
|
||
|
|
5a09e0e787 |
fix(showcase): pin ag2<1.0.0 to unblock CI after upstream module rename
ag2 1.0.0 (published 2026-07-27T00:58:37Z) renames its top-level module
from `autogen` to `ag2` and ships no `autogen/` package at all. The ag2
integration floats on `ag2[openai,ag-ui]>=0.9.0`, so every fresh CI
resolution now picks up 1.0.0 and collection fails at:
src/agents/_multimodal_normalize.py:75
from autogen.ag_ui import AGUIStream, RunAgentInput
E ModuleNotFoundError: No module named 'autogen'
This breaks `Python unit tests (3.10)` and `(3.12)` in showcase_validate
on main and on every open PR. It is time-based rather than commit-based:
main only looks green because it has not re-run since the release.
Pin rather than migrate. 1.0.0 is not a rename -- it deletes the stable
`autogen/ag_ui/adapter.py` and promotes the former `autogen.beta.ag_ui`
stream into its place, dropping `AGUIStream(event_interceptors=...)` and
replacing `dispatch(context=...)` with `dispatch(variables=...)`. Since
`_multimodal_normalize.py` overrides `dispatch` and forwards `context=`,
rewriting the import path alone would swap an import error for a runtime
TypeError. A real migration needs to port ~20 `autogen` import sites and
re-verify every ag2 demo cell, which is not an urgent-unblock change.
|
||
|
|
a9ce37b75c |
fix(showcase/ms-agent-dotnet): resolve OPENAI_API_KEY-first via ApiKeyResolver
Port ms-agent-harness-dotnet's ApiKeyResolver into ms-agent-dotnet so the 15 agent factories resolve the OpenAI credential as OPENAI_API_KEY (env) -> config[OPENAI_API_KEY] -> GitHubToken, and the endpoint via OPENAI_BASE_URL -> default, instead of hardcoding configuration["GitHubToken"] per agent. Previously ms-agent-dotnet's main chat clients authenticated only with the GitHub token. On a fixture-miss fall-through, aimock proxies to real api.openai.com, which rejects the GitHub token (invalid_api_key, surfaced as 502). ms-agent-harness-dotnet already works because it resolves OPENAI_API_KEY first; this brings ms-agent-dotnet to parity so its fall-through returns 200. Interim showcase-side mitigation while the aimock cross-provider guard (PNI-108, CopilotKit/aimock#340) is deferred. - Copy agent/ApiKeyResolver.cs verbatim from ms-agent-harness-dotnet (code copy, no new dependency) - Rewire 15 factories + A2uiSecondaryToolCaller to ResolveApiKey/ResolveEndpoint; drop the dead per-file DefaultOpenAiEndpoint const - Add tests/ApiKeyResolverTests.cs (precedence, mock-endpoint fallback, non-mock fail-fast) Verified: dotnet build 0/0, dotnet test 76/76, whitespace format clean, and a live OpenAI call through the resolver returned 200. |
||
|
|
063750a19e |
fix(react-ui): reduce sidebar CSS specificity (#5805)
## Summary - lowers CopilotSidebar-scoped CSS selector specificity with `:where(.copilotKitSidebar)` - keeps the existing sidebar styles and layout behavior unchanged - adds a regression test to keep sidebar selectors easy for app CSS to override ## Root cause Sidebar-specific selectors such as `.copilotKitSidebar .copilotKitWindow` were more specific than ordinary user overrides like `.copilotKitWindow`, so user CSS could lose precedence in Chrome. Fixes #263. ## Validation - `pnpm -C packages/react-ui test` - `pnpm exec oxfmt --check packages/react-ui/src/css/sidebar-specificity.test.ts packages/react-ui/src/css/window.css packages/react-ui/src/css/header.css packages/react-ui/src/css/input.css` - `pnpm exec oxlint packages/react-ui/src/css/sidebar-specificity.test.ts` |
||
|
|
21cd27ce50 |
fix(react-core): stop CopilotPopup remounting chat on resize (#6173)
## What does this PR do? Fixes a bug where resizing a `CopilotPopup` (v2) rescrolls the message list from top to bottom on every resize, regardless of the user's scroll position. **Root cause.** `CopilotPopup` built its `chatView` override component inside a `useMemo` keyed on `width`/`height`. When those props change — which happens continuously for consumers driving them from a drag-to-resize handle that commits new dimensions on `mouseup` — `useMemo` returns a **new component function**. That function is passed to `CopilotChat` as the `chatView` slot and rendered via `React.createElement(newType, …)`. Because React sees a new element **type** at that position, it unmounts and remounts the entire chat subtree rather than re-rendering it. The remount resets the scroll container's `scrollTop` to `0`, after which the default `initial="smooth"` auto-scroll animates the list from the top back down to the bottom — the visible "rescroll" on every resize. **Fix.** Give the override a stable module-scope identity so its type never changes, and pass the popup shell props (`header`, `toggleButton`, `width`, `height`, `clickOutsideToClose`, `defaultOpen`) through React context instead of baking them into the override via `useMemo` deps. Resizing is now a plain style update on `CopilotPopupView` — no remount — so scroll position is preserved. `Object.assign` still copies the `CopilotChatView` static slot members onto the override so callers reaching them through the namespace continue to work. **Test.** Adds a regression test (`CopilotPopup.resizeRemount.test.tsx`) that injects a mount-counting component into the chat subtree and asserts its mount count stays at `1` across several `width`/`height` changes. It fails on the pre-fix code (one extra mount per resize — observed `4` for 3 resizes) and passes with the fix. Scope is limited to `packages/react-core`: ``` packages/react-core/src/v2/components/chat/CopilotPopup.tsx | 124 +++++++++------ packages/react-core/src/v2/components/chat/__tests__/CopilotPopup.resizeRemount.test.tsx | 98 +++++++++++ 2 files changed, 185 insertions(+), 37 deletions(-) ``` Verification: full `@copilotkit/react-core` unit suite green (1443 tests), `check-types` green. ## Related PRs and Issues - Closes #6172 ## Checklist - [x] I have read the [Contribution Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md) - [ ] If the PR changes or adds functionality, I have updated the relevant documentation — _N/A: internal behavior fix, no public API or documented behavior change_ - [x] "Allow edits by maintainers" is checked |
||
|
|
a76d59ae0b | fix(sdk-python): capture subgraph context from run input (#3886) | ||
|
|
73fe697b15 |
fix(web-inspector): handle empty tool result content (#5603)
## Summary
Fixes the web inspector crash when historical tool result messages have
empty `content`.
Some stored thread tool-result messages can have `content: ""`. The
inspector previously parsed tool result content with
`JSON.parse(msg.content ?? "{}")`, which still passes the empty string
into `JSON.parse` and throws `Unexpected end of JSON input`.
## Changes
-> Treat empty and whitespace-only tool result content as an empty
result object
-> Keep malformed non-empty result content flowing through the existing
parse-error fallback
-> Add regression coverage for empty and whitespace-only tool result
messages in the thread conversation mapper
## Testing
-> `pnpm exec oxlint packages/web-inspector/src/index.ts
packages/web-inspector/src/__tests__/web-inspector.spec.ts`
-> `pnpm -C packages/web-inspector exec vitest run
src/__tests__/web-inspector.spec.ts --pool=threads --testNamePattern
"maps empty tool result content" --reporter=verbose`
## Notes
-> `pnpm nx show project @copilotkit/web-inspector --json` timed out
locally
-> `pnpm -C packages/web-inspector run check-types` is currently blocked
by existing package-level TypeScript errors around explicit `.js` import
extensions and one unrelated existing error
## Related Issue
Closes #5598
|
||
|
|
bde2d99a5b |
fix(showcase/ms-agent-python): keep the user's prompt on the multimodal PDF turn (#6159)
`d6:ms-agent-python/multimodal` has been red in staging and prod since
2026-05-30. Turn 1 (image) passes; turn 2 (PDF) fails. This fixes it —
**without touching the fixture**, because the fixture was never the
problem.
## The verbatim turn-2 error
Backend (`showcase-ms-agent-python`), and reproduced locally:
```
[/multimodal] Streaming failed
openai.InternalServerError: Error code: 503 - {'error': {'message': 'Strict mode: no fixture matched',
'type': 'invalid_request_error', 'param': None, 'code': 'no_fixture_match'}}
The above exception was the direct cause of the following exception:
agent_framework.exceptions.ChatClientException: ("<class
'agent_framework_openai._chat_completion_client.OpenAIChatCompletionClient'> service failed to
complete the prompt: Error code: 503 - {'error': {'message': 'Strict mode: no fixture matched', …
```
Surfaced in the browser as `An internal error has occurred while
streaming events.`, with the probe reporting `failure_turn: 2`,
`turns_completed: 1`.
## Request-shape diagnosis
This reads like a fixture gap and is not one. I pulled the **actual
outbound request** off the local aimock's `GET /__aimock/journal` during
a failing run. Turn 2, verbatim (bodies elided):
```
[0] role=system "You are a helpful assistant. The user may attach images or documents…"
[1] role=user "can you tell me what is in this demo image I just attached"
[2] role=user [image_url <data:image/png;base64,iVBORw0K…>]
[3] role=user [image_url <data:image/png;base64,iVBORw0K…>]
[4] role=assistant "The attached image is the CopilotKit logo — a clean, geometric mark…"
[5] role=user "can you tell me what is in this demo pdf I just attached"
[6] role=user "[Attached document]\nCopilotKit Quickstart\nAdd AI copilots to your React…"
[7] role=user "[Attached document]\nCopilotKit Quickstart\nAdd AI copilots to your React…"
```
One logical user turn arrived as **three separate user messages**, and
the *last* one carries only the flattened document — the question is
nowhere in it. That is why aimock's strict mode refused it:
`userMessage` is a substring match against the last user turn, and the
last user turn was a PDF dump.
**Root cause:** `agent_framework_openai` emits **one OpenAI message per
`Content`**. `_chat_completion_client._prepare_message_for_openai`
builds a fresh `args` dict on every iteration of its content loop, so a
user `Message` carrying `[prompt_text, flattened_doc_text]` serialises
to two consecutive user messages — prompt-only, then document-only.
`_PdfFlattenChatMiddleware` was appending the flattened `[Attached
document]` text as a *second* text `Content` beside the prompt, which is
exactly the shape that gets split.
Two corroborating details that make the mechanism airtight:
- **Why turn 1 (image) passes.** aimock already skips *text-less*
trailing user messages (`getLastUserText` in `router.ts`, whose comment
documents this exact MS Agent Framework behavior). The image turn's
split-off trailing message has no text at all, so aimock falls back to
the prompt message and matches. The PDF turn's trailing message *does*
have text — the document — so there is nothing to skip past.
- **Why `langgraph-python` is green** doing the identical `[Attached
document]` flattening: LangChain keeps multiple text parts *inside one
message* rather than splitting them into separate messages.
This is a product bug, not a mock artefact. Against a real LLM it would
not 503 — the model would just answer the wrong thing, because the
question is buried behind a document dump instead of being the current
turn.
## The fix
`showcase/integrations/ms-agent-python/src/agents/multimodal_agent.py`
1. **Merge** the flattened document *into* the message's existing prompt
text content instead of appending it as a second content. The turn stays
a single text content and serialises to a single user message:
`"<prompt>\n[Attached document]\n<body>"`.
2. The merge **copies** the prompt `Content` rather than mutating it.
This is load-bearing: the middleware restores the original `contents`
list after `call_next`, and that restore only undoes the *list* swap —
an in-place mutation would leak the raw PDF body into the AG-UI
`MESSAGES_SNAPSHOT` and render a wall of PDF text in the user's chat
bubble. There is a test for this.
3. **Attachment-only turns** (a PDF with no question) still work: with
no text content to merge into, the flattened document stands alone as
the message body.
4. **Dedupe identical flattened blocks.** The page's
`LegacyConverterShim` appends a legacy `binary` mirror alongside every
modern attachment part, so the same PDF reached the middleware twice and
its body was being sent to the model twice (visible as the duplicated
`[6]`/`[7]` above). Now emitted once.
Post-fix outbound turn 2, same journal endpoint:
```
[5] role=user "can you tell me what is in this demo pdf I just attached\n[Attached document]\nCopilotKit Quickstart\nAdd AI copilots to your React application with CopilotKit…"
matched fixture userMessage: "can you tell me what is in this demo pdf I just attached"
```
One user message, prompt intact, document intact, emitted once.
## The fixture is untouched
```
$ git diff --stat origin/main -- showcase/aimock/
(empty)
```
The existing `userMessage` match key was always correct; the corrected
request shape is what satisfies it. Relaxing or re-recording the fixture
to match the broken request was an explicit non-goal — it would have
made the cell actively certify a model that never sees the user's
question.
## Same-pattern audit
- `_PdfFlattenChatMiddleware` is the **only** `ChatMiddleware` in
`ms-agent-python`, and the only place in the integration that constructs
`Content` or reassigns `message.contents` (`grep` for `ChatMiddleware` /
`Content.from_text` / `.contents =` across `src/` returns hits in this
one file only). No second instance of the pattern to fix.
- `ms-agent-python` is the only MS-Agent-Framework Python integration
doing PDF flattening — `ms-agent-dotnet` has a multimodal e2e spec but
no Python agent. The other `[Attached document]` implementations
(`langgraph-python`, `langgraph-fastapi`, `agno`, `claude-sdk-python`,
`langroid`, `pydantic-ai`, `langgraph-typescript`, `built-in-agent`) run
on frameworks that do not split a message's contents into separate wire
messages, so they are not exposed to this. The upstream
one-message-per-`Content` behavior is pinned by a dedicated test, so if
it ever changes we find out by that test failing rather than by a silent
regression.
- The file is a regular per-integration file, not a `shared/` symlink
(`git ls-files -s` → `100644`). No shared code touched;
`validate-shared-symlinks.ts` confirms no new erosion.
## Red / green / control
All three on the real probe surface, from a clean worktree at
`origin/main` `38613623f4`.
### RED — before the change
```
$ bin/showcase test ms-agent-python:multimodal --d6 --direct --verbose --cycle --isolate
[conversation-runner] turn 1/2 — assistant settled { bubbleIndex: 0, textLength: 100, hasAssertions: true }
[conversation-runner] turn 1/2 — assertions passed
[conversation-runner] turn 2/2 — sending message { inputLength: 29, timeoutMs: 60000 }
[conversation-runner] turn 2/2 — FAILED {
errorCategory: 'assertion-failed',
turnsCompleted: 1,
elapsedMs: 1577,
bodyTextLength: 421,
hasTextarea: true,
hasErrorBoundary: false
}
[warn] CVDIAG component=harness-d6 boundary=fixture-match … status=miss … error=chat errored: copilot-error-banner visible — An internal error has occurred while streaming events.
[info] probe.e2e-full.service-complete {"slug":"ms-agent-python","passed":0,"failed":1,"skipped":0,"incapable":0,"total":1,"state":"red","durationMs":9384}
✗ d6:ms-agent-python red (9.5s)
multimodal: chat errored: copilot-error-banner visible — An internal error has occurred while streaming events.
0 passed, 1 failed (9.5s)
⚠ Tests failed for ms-agent-python:multimodal (exit 1)
```
Evidence the outbound request lacked the prompt — aimock journal from
that run, 8 entries, `200,503,503,503,200,503,503,503` (2 attempts × 3
retries on turn 2):
```
[5] role=user STRING "can you tell me what is in this demo pdf I just attached"
[6] role=user STRING "[Attached document]\nCopilotKit Quickstart\nAdd AI copilots to…"
[7] role=user STRING "[Attached document]\nCopilotKit Quickstart\nAdd AI copilots to…"
status: 503
```
### GREEN — after the change, fixture unchanged
```
$ bin/showcase test ms-agent-python:multimodal --d6 --direct --verbose --rebuild --keep --isolate
[conversation-runner] turn 1/2 — assistant settled { bubbleIndex: 0, textLength: 100, hasAssertions: true }
[conversation-runner] turn 1/2 — assertions passed
[conversation-runner] turn 2/2 — assistant settled { bubbleIndex: 1, textLength: 233, hasAssertions: true }
[conversation-runner] turn 2/2 — assertions passed
[conversation-runner] conversation completed successfully { turnsCompleted: 2, totalDurationMs: 8279 }
[info] probe.e2e-full.feature-complete {"slug":"ms-agent-python","featureType":"multimodal","pass":true,"durationMs":8788}
[info] probe.e2e-full.service-complete {"slug":"ms-agent-python","passed":1,"failed":0,"skipped":0,"incapable":0,"total":1,"state":"green","durationMs":10187}
✓ d6:ms-agent-python green (10.5s)
1 passed (10.5s)
✓ Tests passed for ms-agent-python:multimodal
```
Both turns pass. aimock journal for that run: **2 entries, statuses
`200,200`** (down from 8 entries with six 503s — no retries needed).
**The fixture was not modified**; `git diff origin/main --
showcase/aimock/` is empty and the diff is two files, both under
`showcase/integrations/ms-agent-python/`.
### CONTROL — an already-green integration, same command, same stack
```
$ bin/showcase test langgraph-python:multimodal --d6 --direct --isolate
[conversation-runner] turn 2/2 — assistant settled { bubbleIndex: 1, textLength: 233, hasAssertions: true }
[conversation-runner] turn 2/2 — assertions passed
[conversation-runner] conversation completed successfully { turnsCompleted: 2, totalDurationMs: 8395 }
✓ d6:langgraph-python green (9.1s)
1 passed (9.1s)
✓ Tests passed for langgraph-python:multimodal
```
Local harness, shared probe, shared frontend and fixtures are all sound
— the red was specific to this integration.
## Covering test
`showcase/integrations/ms-agent-python/tests/python/test_multimodal_pdf_prompt.py`
— 7 tests. Not fakes: each one drives the real
`_PdfFlattenChatMiddleware` and then the real
`OpenAIChatCompletionClient._prepare_message_for_openai`, and asserts
against the actual OpenAI wire payload. The PDF is the bundled
`public/demo-files/sample.pdf` through real `pypdf`, and the prompt
asserted on is **read out of the real aimock fixture** rather than
hardcoded, so the test fails if either side drifts.
Test-level red→green (stash the source change, keep the tests):
```
# pre-fix
FAILED test_multimodal_pdf_prompt.py::test_pdf_turn_last_user_message_contains_the_prompt
FAILED test_multimodal_pdf_prompt.py::test_pdf_turn_serialises_to_a_single_user_message
FAILED test_multimodal_pdf_prompt.py::test_duplicate_pdf_parts_are_flattened_once
3 failed, 4 passed in 2.37s
```
with the primary failure reading:
```
AssertionError: expected the PDF turn to serialise to 1 user message, got 2:
['can you tell me what is in this demo pdf I just attached',
'[Attached document]\nCopilotKit Quickstart\nAdd AI copilots to']
```
```
# post-fix — full integration suite (6 pre-existing CVDIAG + 7 new), CI's exact invocation
$ PYTHONPATH=".:src" python -m pytest tests/python/ -q
13 passed in 2.40s
```
Coverage: prompt survives to the final user turn; the turn stays one
user message; the upstream one-message-per-`Content` split is pinned;
original `contents` restored and the prompt `Content` not mutated;
duplicate mirror parts flattened once; attachment-only turn still
flattens; image turn left byte-identical.
## Pre-push
`validate-parity.ts` 20/20 pass · `validate-shared-symlinks.ts` no new
erosion · `aimock-fixtures.test.ts` 842 pass · full `tests/python/`
suite 13 pass · lefthook `lint-fix` + `commitlint` clean · Python lines
≤88 cols matching the file's existing style · no lockfile churn, two
files in the diff.
## Scope
One cell, one middleware, one integration. The other five red
`multimodal` cells from the same sweep have five different root causes
and are not addressed here.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01PYdjeveT8Xof9TyHWMLoJr
|
||
|
|
edc59c476c |
fix(showcase): cold-load gray chip was masking genuinely-red cells (#6156)
> **Scope, stated up front.** This PR fixes **one** of the ways a
genuinely-red
> showcase cell can render as something other than red: the cold-load
`signal`
> polarity. It does **not** eliminate the symptom. **Nine further
masking
> mechanisms remain** and are enumerated, with measured counts, under
> [What this PR does NOT fix](#what-this-pr-does-not-fix). Please read
that
> section before treating a green dashboard as evidence of a green
fleet.
## The bug
Absence of evidence about *why* a cell failed was erasing the fact that
it failed.
`classifyRung` opened its red branch with:
```ts
if (!raw.signalKnown) {
return { ...base, contribution: "NO_DATA", rawStatus: null };
}
```
`signalKnown` is `false` when the dashboard's bulk fetch projected the
`signal`
blob away. So a **genuinely RED rung whose infra-vs-product attribution
was merely
PENDING got classified `NO_DATA` and painted gray** — the muted "no live
probe
data" treatment — while the depth strip beside it, built from the raw
fold,
correctly rendered `1P ✗`. One model object simultaneously said
`d5.status ===
"red"` and `chipColor === "gray"`.
Two things make that polarity wrong:
1. **`signalKnown === false` never meant "known to have no signal."**
PocketBase
omits the `signal` key *only* under a `fields=` projection; a row that
genuinely has no signal arrives as `null`, and `null !== undefined`. The
flag
is pure provenance: "this row came from a projected fetch." It was never
evidence the failure was infra.
2. **The guess was wrong on the overwhelming majority.** Of the
non-green rows in
production, **94.2% are product-class and 5.8% infra-class** (measured
today
against prod PocketBase using the classifier's own imported
`signalHasInfraErrorClass` predicate: 3082 rows, 359 red, 21 infra-class
—
`driver-error` 11, `abort` 10 — 338 product-class). Graying every
unattributed
red lost 338 real failures to suppress 21 false alarms. A masked red is
never
investigated; a false alarm is looked at once and closed.
This was **not** a first-paint race. The gray state is a fully-rendered,
confident-looking wrong answer that persists until the probe's next
sweep rewrites
that specific row and the SSE delta redelivers it with `signal`, and it
recurs on
**every** reload, because the projection strips `signal`
unconditionally.
**Worst case for the routine per-cell dimensions is ~60 min**, from the
hourly
`e2e-demos` / `starter_smoke` / `d6-all-pills-e2e` crons in
`harness/config/probes/*.yml`; drift dimensions are effectively
unbounded. *(An
earlier revision of this body claimed "~29 min observed, ~14 min mean".
Both were
withdrawn during review: "~29 min" was derived from the `*/30` tier and
missed the
hourly tier entirely, and "~14 min mean" had no derivable basis. Do not
cite them.)*
## Measured effect
PR #6169's exhaustive sweep — **279,936 rung combinations** (7 status
rows × 6 row
variants), the same map fed to the pre-fix and post-fix engines and
diffed
field-by-field — puts the direction of this change beyond argument:
- **74,256 cells change**, and **every one moves toward *more* red.**
- **Zero** flips toward less red: no `red→gray`, no `red→green`, no
`amber→green`
anywhere in the diff.
- INV2–INV6 hold across the full sweep on the branch engine (0
violations).
Red-green on **live production data** (local `next dev` wired to prod
PocketBase,
driven with Playwright, screenshots visually inspected — DOM
measurements alone are
not trusted for visual verification; both captures back-to-back on the
same
underlying rows, only the code changing):
| Band | before | after |
|---|---|---|
| green | 632 | **632 — unchanged, no green regression** |
| amber | 0 | 0 |
| **red** | **0** | **51** |
| no data | 76 | **25** |
`0 red → 51 red`, `76 → 25 no data` — 51 recovered, arithmetic closes.
Red on
**first paint**, no gray plateau.
## The fix — two parts
### (a) Flip the fail-safe polarity — `cell-model.contribution.ts`
A red rung with unknown infra-ness now classifies `FAIL_FRESH`. Graying
a red
requires **positive infra evidence**: `hasNonInfraRed` is false only
when every
contributing red row's blob actually carries an `INFRA_ERROR_CLASSES`
attribution,
and signal-provenance is retained as an explicit second precondition so
a missing
blob can never be read as an infra attribution even if
`signalHasInfraErrorClass`
is loosened later.
```ts
if (fold.redSignalKnown && !fold.hasNonInfraRed) {
return { ...base, contribution: "INFRA_RED_FRESH", rawStatus };
}
```
`redSignalKnown` is derived inside `foldFamily`'s **red-row**
bookkeeping, not
family-wide, so a projected-away GREEN sibling cannot suppress a
legitimate infra
gray. (The original family-wide flag made `INFRA_RED_FRESH` effectively
**unreachable in the browser for every multi-row family** — D4, any
multi-pill
D5/D6; only single-key D3 was unaffected, which is why every
pre-existing fixture
missed it. Fixed by round-1 unit G1.)
This is the durable fail-safe: any future projection or fetch regression
now
degrades toward **over-reporting** failure rather than silently hiding
it.
### (b) Make first paint correct — `useLiveStatus.ts`
The supplemental `signal` fetch is widened from the comm-error
aggregates to a
**union**: the existing clause OR `state != "green"`.
```
before: filter=(dimension = "d6" || … || dimension = "d5-single-pill-e2e") && key !~ "%/%"
after: filter=((dimension = "d6" || … || dimension = "d5-single-pill-e2e") && key !~ "%/%") || (state != "green")
```
The existing fetch was not a viable vehicle as-is: it excluded these
rows twice
over — dimension `d5` is absent from `FLEET_COMM_AGGREGATE_DIMENSIONS`
entirely,
*and* its `key !~ "%/%"` predicate excludes every per-cell ladder-rung
key.
Relaxing only the `%/%` predicate would still have missed them.
**Why widen the existing fetch rather than add a second one:** it reuses
the
freshness guard (`supplementalRowIsOlder`) and the chimera-avoidance
merge
verbatim — that merge already reasons carefully about a newer bulk row
vs an older
signal-bearing twin, and duplicating it would be the risky part. It also
keeps
first paint at one extra request instead of two.
**Why scope by STATE, not by key:** signal-provenance is consulted
**only** in
`classifyRung`'s red branch — green and degraded paths never touch it —
so only
non-green rows can change a chip verdict. `!= "green"` rather than an
explicit
red/degraded list is deliberate: it also catches an out-of-vocabulary
state, which
`rankOfState` ranks **worst** — so the rows that matter most can never
fall outside
the fetch. The comm-error clause is kept as an independent disjunct, not
replaced:
a **green** aggregate row can still carry an active comm-error blob.
The fetch is page-capped at `MAX_INITIAL_FETCH_PAGES = 20`, projected to
`STATUS_LIST_FIELDS,signal`, and non-fatal at the fetch site —
truncation is handled
**asymmetrically**: supplemental truncation degrades (warn, use what
arrived), bulk
truncation throws. (Round-1 unit FETCH; the fetch was originally
unbounded, and its
rejection propagated to `setRows([])`, so an *enrichment* failure
blanked the whole
matrix.)
**Rejected: suppressing the chip while pending.** A blank or spinning
chip for up
to an hour on a large slice of the matrix hides reds exactly as
effectively as a
gray one, while additionally looking broken.
### What each part buys
Real engine over all 3082 production rows, three wires built from the
identical
row set *(green / amber / red / gray over the 708 catalog cells the band
counts)*:
| Wire | pre-fix engine | post-fix engine |
|---|---|---|
| FULL (server / `/api/matrix`) | 596 / 37 / 51 / 24 | 596 / 37 / 51 /
24 |
| COLD, pre-fix projection | **596 / 0 / 0 / 112** | 596 / 37 / 51 / 24
|
| COLD, post-fix projection | 596 / 0 / 49 / 63 | 596 / 37 / 51 / 24 |
- The pre-fix browser wire collapsed **88 cells** (51 red + 37 amber)
into gray.
The entire red *and* amber population was zeroed, because the
`!signalKnown`
early return preempted `FAIL_FRESH`, `FIRST_STRIKE_FRESH` **and**
`INFRA_RED_FRESH` alike.
- **(b) alone is not sufficient**: it recovers 49 of 51 reds and
**zero** of the
37 ambers, leaving 39 cells still mis-painted.
- **(a) is what delivers correctness** — with it, all three wires equal
server
truth exactly.
- **(b) is what keeps (a) from having to guess.** Today (a)-alone
happens to cost 0
false-positive cells, because no cell's verdict-determining rung is
currently a
pure infra red. That is luck, not a guarantee: prod holds 21
infra-classed red
rows. (b) supplies the actual evidence so those stay correctly gray, and
it also
feeds `decodeCellCommError` for non-green per-cell rows.
## Value test — per cell, all 6 (≥3 required)
Every affected cell verified individually, not inferred from a shared
symptom.
Live DOM on the real surface, plus the real engine driven offline over
all 3082
production rows through faithful replicas of the pre- and post-fix
browser wire:
| Cell (`d5:<slug>/multimodal`) | PB row | Server truth | Cold PRE |
Cold POST | Live chip PRE | Live chip POST | Flipped |
|---|---|---|---|---|---|---|---|
| `agno` | red, fc 1817 | red | **gray** | red | gray `D4` + `1P ✗` |
**red `D4`** | ✅ |
| `crewai-crews` | red, fc 1716 | red | **gray** | red | gray `D4` + `1P
✗` | **red `D4`** | ✅ |
| `llamaindex` | red, fc 2005 | red | **gray** | red | gray `D4` + `1P
✗` | **red `D4`** | ✅ |
| `mastra` | red, fc 1806 | red | **gray** | red | gray `D4` + `1P ✗` |
**red `D4`** | ✅ |
| `ms-agent-python` | red, fc 1710 | red | **gray** | red | gray `D4` +
`1P ✗` | **red `D4`** | ✅ |
| `built-in-agent` | red, fc 1803 | red | **gray** | red | gray `D4` +
`1P ✗` | **red `D4`** | ✅ |
6/6 flip. All six are `errorClass: conversation-error` — a real probe
conversation
failure, not in `INFRA_ERROR_CLASSES`, so `FAIL_FRESH` is the correct
classification. (Prod figures; `built-in-agent` is green on staging, so
staging
shows 5.)
**Depth distribution is byte-identical across the pair** (`D6:632 D5:0
D4:71 D3:0
D0:5` both times). The fix changes only the *colour*, never the depth —
the `D4`
label on these chips was always correct, so this must not be
mis-validated by
looking for a depth change.
## Tally-site audit
Every tally is downstream of `chipColor`, so all self-correct — none
needed
refactoring. All verified on the live surface post-fix:
| Site | Mechanism | Pre-fix | Post-fix | Verified |
|---|---|---|---|---|
| `feature-grid.tsx` `computeColumnTally` | gray-skip | `✗ 0` | `✗ 8`
(Agno) | ✅ header DOM |
| `feature-grid.tsx` `computeColumnTallyDetail` | gray-skip | empty list
| 8 features incl. **Attachments** | ✅ panel opened via
`tally-trigger-red` |
| `page-stats.ts` `computeHealthStats` | `case "gray": noData++` | `0
red / 76 no data` | `51 red / 25 no data` | ✅ band DOM + screenshot |
| `page-stats.ts` `computeDepthDistribution` | — (no gray gating at all)
| `D4:71` | `D4:71` | ✅ unaffected |
| `page-stats.ts` `computeD6Stats` | `d6Effective` | `null` → gray |
`"red"` → red | ✅ golden fixture |
**No other `chipColor` consumer changes behaviour.**
`d0-gone-predicate.ts` gates
the outage alerter on `chipColor === "red"`, but `d0-gone-monitor` reads
*full rows
with `signal`*, so provenance is always known there — as it is for
`/api/matrix`.
The polarity flip is reachable **only** where rows are projected, i.e.
exclusively
the browser cold-load path.
## A test asserted the buggy behaviour
`api-matrix-equivalence.test.ts` asserted:
```ts
expect(serverChip).toBe("red");
expect(coldLoadChip).toBe("gray");
expect(serverChip).not.toBe(coldLoadChip);
```
commented as "the state the browser converges to after its supplemental
`signal`
fetch … the more accurate answer." That comment is factually wrong — the
supplemental fetch resolved in ~1 s and structurally excluded those rows
twice over
— and the assertion pinned the defect as intended behaviour. **This is
why CI never
caught it.** Read in full, it is asserting the defect, not an adjacent
legitimate
case: the fixture is a genuine product red (`errorDesc: "assertion
failed: wrong
answer"`, no infra class) with `signal: undefined` on the red rung.
Inverted to assert **red on both paths**, with the fail-safe direction
explained
inline. Same for the harness's `§7 I5` case (`cell-model-v2.test.ts`)
and
`classifyRung`'s unit case. Four golden-master fixtures
(`pos-d{3,4,5,6}-red-signal-unknown`) re-frozen.
Negative controls added so the flip can't over-reach: an infra-classed
red **still**
grays (positive evidence present), and `§7 I4` (infra-only red → gray,
`isRegression` false) still passes untouched.
## The structural lever: coherence invariant INV7
`cell-model.coherence.test.ts` asserted INV1–INV6, all of which relate
`chipColor`
only to the other **chip-side** outputs. **None inspected the
`d3`/`d4`/`d5`/`d6`
depth pills.** So the engine could return one object saying both
`d5.status ===
"red"` and `chipColor === "gray"` with every invariant intact — which is
exactly
what happened, on every cold load.
INV7: **a gray chip above a red depth pill requires positive infra
evidence on
every contributing red row.** Red-greened against the old polarity — it
fails on 3
fixtures with a legible message:
```
pos-d5-red-signal-unknown: INV7 gray chip over a red pill requires POSITIVE infra
evidence on every red row — d5:acme/agentic-chat has none (signal STRIPPED (pending)).
A red whose cause is merely unknown must render red, not gray.
```
It closes the gap in the **only** safe direction: by fixing the chip
upward, never
by muting the strip. Deriving the pills from the classified
contributions — the
superficially cleaner unification — would render `1P ✗` as `1P ?` on
cold load and
destroy the only on-page element that was telling the truth, converting
a visible
inconsistency into an invisible failure.
INV7's own limitations are disclosed under
[Known limitations of the guards
themselves](#known-limitations-of-the-guards-themselves).
---
# Review-round fixes folded in
Three full 12-reviewer review rounds were run against this branch. Round
1
contributed five units plus an integration fix; round 2 contributed four
more. The
loop was **deliberately stopped** after round 3 by explicit decision
rather than
running a fourth; round 3's findings are catalogued (132 items in
`DEFERRED-r3.md`) and routed to the follow-up epic, not silently
dropped.
## Round 1 — five units
| # | Commit | Concern | Surface |
|---|---|---|---|
| G1 | `2a68282d` | Signal-provenance was derived family-wide but gated
a red-row-scoped predicate. Precondition moved into `foldFamily` as
`redSignalKnown`; `RawRung.signalKnown` **removed entirely** with all
its plumbing. | production + tests |
| A5 | `ebd7ac68` | The autocancel fake servers ignored
`filter`/`fields`, so the supplemental fetch's leftover-append silently
repaired a dropped bulk page — three pagination guards passed under
every mutation. Servers rebuilt to evaluate the query. | test-only |
| FETCH | `f5178765` | The supplemental fetch was unbounded (`for(;;)`),
unprojected, and its rejection propagated to `setRows([])`. Now capped,
projected, and non-fatal at the fetch site. | production + tests |
| INV7 | `3dea9f64` | INV7 was quantified over the whole live map rather
than the cell's contributing keys, and had no `isStaleCell` carve-out —
it failed two classes of legitimate rendering. Scoped, exempted, plus an
anti-vacuity guard. | test-only |
| DOCS | `2797618c` | Stale/unanchored numeric claims (row counts, byte
estimates, re-delivery window) corrected against production and given
their source queries. | comments only |
| — | `938be2ac` | **Integration fix.** A5 and FETCH disagreed about how
a supplemental request is *identified*; all three fake servers
misclassified it as a bulk page and the whole autocancel suite failed
3/3. Both dashboard suites now key on `signal` being IN the projection.
| test-only |
## Round 2 — the four integrated units
Integrated in order Q2 → Q1 → Q3 → Q4. Every guard was re-mutated
**after**
integration and reproduced its pre-integration result; no guard was
disarmed by
the merge.
| # | Commit(s) | Concern | What it proved | Surface |
|---|---|---|---|---|
| **Q2** | `b8b75b7d` | The dashboard's `useLiveStatus` test doubles
carried **three divergent implementations** of the same PocketBase query
evaluator, and the nominally-shared one was the weakest. Collapsed to
exactly one, every semantic a measured observation from a real
PocketBase v0.22.21 server. Deleted autocancel's 380-line private copy.
`evaluatePbList` **cannot throw**, so a parse error can no longer send
NO response and hang the hook to a 20 s `waitFor` death. | Six mutations
(short page dropped in wave merge, boundary logic removed, merge order
reversed, supplemental filter broken, supplemental filter widened to
`""`, bulk filter selects nothing) each reproduced their pre-integration
RED count exactly — 3/1/2/6/5/6 failures. The consolidated evaluator is
strictly stronger, not merely shorter. | test-only (−488 lines net in
the doubles) |
| **Q1** | `75fdfb3a` + `ed10b61f` | `truncated` meant "cap exhausted",
so a **complete** read that happened to end exactly on the cap page was
reported as truncated — the bulk caller threw, burned its reconnects and
landed on a blank dashboard behind an offline banner, on every retry and
every reload. `truncated` now means exactly "there are rows we did not
read", proven by ONE lookahead read of page cap+1 whose rows are
discarded and only whose emptiness is load-bearing. Algorithm hoisted to
an exported `paginateStatusPages(readPage)`. | Lowering the cap 20→10
and 20→5 each produces **10 failed / 6 passed**, where the pre-fix code
was **6/6 GREEN** — i.e. the guard was previously disarmed. A complete
10,000-row dataset renders `live`, `truncated === false`, seeded red
cell still `chipColor: "red"`, page set exactly `[1..21]`; 10,001 rows
still throws with `status: "error"` and "exceeded" in the message. |
production + tests |
| **Q3** | `49dea099` | INV7 false-failed on the ordinary I1 ladder-gap
shape ("no `e2e:` row + red `chat:<slug>`"). Added a carve-out keyed on
`ladderGapDepth`, **one-directional** (a gap *above* the red excuses
nothing) and never firing without a witness rung — plus a two-sided
canary that fails loudly the moment the underlying gap-break is fixed. |
Deleting the `isStaleCell` exemption fails exactly the U8 force-gray
case (the exemption is load-bearing, not decorative). Changing the
engine's gap-break `break` → `continue` fires the **two-sided canary**
with its intended message (`expected 'red' to be 'gray'`) — so the
allowance cannot go on silently excusing a shape the engine no longer
produces. Gapless and gap-above-red incoherence still FAIL; the
legitimate gap shape still PASSES. | test-only |
| **Q4** | `b04330ab` | Three explicitly load-bearing family folds were
invisible at golden-master level — `maxNonInfraRedFailCount`'s MAX,
`allRedSoftClass`'s EVERY quantifier, and `redSignalKnown`'s RED-ROW
scope — because no fixture anywhere carried two *differing* red rows. 5
new fixtures, baseline 56→61, 140 insertions / 0 deletions. | Each of
three mutations now fails via **exactly one** fixture:
`Math.max`→`Math.min` ⇒ `hetero-d4-red-failcount-max`; `every`→`any` ⇒
`hetero-starter-mixed-soft-hard`; `redSignalKnown` red-row→family scope
⇒ `hetero-d4-green-stripped-infra-red`. Precise attribution, not a
shotgun. | test-only |
One merge conflict, in `useLiveStatus.supplemental-bounds.test.tsx`: Q2
deleted the
file-local `classifyRequest` helper on the same lines where Q1 added its
new
`PAGES_THROUGH_LOOKAHEAD` constant. Resolved by keeping **both** intents
rather than
taking a side.
Baseline golden-master key count is **61**, and the completeness check
(`has exactly
one baseline entry per fixture (no stale/missing)`) runs, so a fixture
added without
a baseline entry fails rather than being skipped.
---
# What this PR does NOT fix
**This PR does not fully eliminate the original symptom.** It fixes the
`signal`-polarity masking mechanism and makes the cold load correct. A
genuinely-red
cell can still render **gray, green, or amber** by nine other routes.
All nine were
found by review of this branch and are catalogued, not speculative.
### Quantified by the #6169 sweep (279,936 combinations)
| Mechanism | Rung combinations |
|---|---|
| **M1** — ladder gap-break: `scanWorst` folds the FIRST `ABSENT`/`STUB`
rung as `ABSENT` and **`break`s**, so a fresh non-infra RED on a rung
*above* the gap is never folded and paints gray | **17,088** |
| **M2** — D6 outside the `scanWorst` walk: a red D6 never reaches the
chip | **2,048** |
| **M3** — gap not below the red: the remaining gray-over-red shapes |
**192** |
| | **19,328 total, 0 unexplained** |
M1 is highly reachable in practice: `chat:<slug>` / `tools:<slug>` are
integration-scoped, so one red `chat:` row plus an unemitted
`e2e:<slug>/<featureId>`
row (new feature, rotation slot, D3 driver never ran) is a gray chip
over a red D4
pill. `combine.ts` is deliberately untouched here — fixing any of M1–M3
re-verdicts
cells across the matrix and therefore needs its own PR with a
golden-master
**baseline re-freeze** and a **live value test**. Q3's INV7 carve-out is
silent about
M1 **on purpose and only about that**, and its two-sided canary is the
tripwire that
forces the carve-out to be deleted the moment M1 is actually fixed.
### Found in round 3, not quantified by the sweep
4. **Prototype-chain property lookups invert the anti-masking guard.**
`STATE_RANK` / `rankOfState`, `WORST_STATE_RANK` / `worstStateRank`,
`firstStrikeConfig[kind]` and `CATALOG_TO_D5_KEY` are plain object
literals read
without an own-property guard, so a runtime value colliding with an
`Object.prototype` member (`constructor`, `toString`, `valueOf`, …)
resolves to an
inherited function that the `??` fallback then never replaces. For
`rankOfState`
the out-of-vocabulary guard **inverts** — an unknown state, which the
code
deliberately ranks worst, instead classifies as an infra red and grays;
`worstStateRank` silently swallows the row entirely. `CATALOG_TO_D5_KEY`
is
unguarded at four further sites in `cell-model.ts`, where the
consequence is a
**whole-matrix render crash**. Fix is
`Object.prototype.hasOwnProperty.call` (or
a prototype-free table) at each site.
5. **Wave truncation bypasses the fail-loud throw entirely.** In the
fan-out merge,
a short page inside a wave causes every page issued *after* it in the
same wave
to be discarded — sound for a stable collection, but the pages resolve
concurrently against a table the harness writes continuously. The
discard
produces no log, no warning, no counter, no `degraded` flip, and
**`truncated`
stays `false`** because it is only ever set by the cap lookahead. So the
bulk
caller's deliberate "a truncated BULK read cannot degrade gracefully …
throw"
posture fires only for cap exhaustion, never for this class of loss. The
missing
rows render as *absent*, which `buildCellModel` resolves to `ABSENT` /
`NO_DATA`
and paints gray — the exact polarity this PR exists to remove.
`INITIAL_FANOUT_BATCH`
is documented as "purely a WIRE-EFFICIENCY knob" but is load-bearing for
correctness: at a larger batch the loss is `INITIAL_FANOUT_BATCH - 1`
pages.
6. **`INITIAL_PAGE_SIZE` above the server clamp grays ~6/7 of the
matrix.**
Termination is the client-side comparison `first.length ===
INITIAL_PAGE_SIZE`,
correct only because PocketBase clamps `perPage` to 500 server-side.
Raise the
constant — the obvious "fewer round-trips" tuning, which the surrounding
comment
invites — and every page returns 500, i.e. *short*, so the loop stops
after page
1 and returns ~500 of ~3100 rows with `truncated: false`. The bulk
caller sees a
complete read, never throws, and ~6/7 of the matrix renders no-data gray
with no
warning and no banner. The `truncated` contract cannot catch it because
the read
genuinely never hit the cap.
7. **An empty bulk page 1 publishes `live` with 0 rows.**
`paginateStatusPages`
returns `{rows: [], truncated: false}` for an empty collection and
`connect()`
then does `setRows([])` + `setStatus("live")` + `setError(null)` with no
empty-collection guard. Every cell renders no-data gray behind a healthy
"live"
indicator. Untested — the smallest fixture anywhere in these files is 3
rows.
The server-side analogue is already a known deferred item.
8. **First-strike de-amplification fires at `fail_count === 0`, painting
a genuine
red AMBER.** The gate is `maxNonInfraRedFailCount < threshold` with
`threshold`
2, so `fail_count` of **0** de-amplifies exactly like 1 →
`FIRST_STRIKE_FRESH` →
amber, i.e. "transient, not yet actionable". The threshold's semantics
are valid
only if a red row's `fail_count` is always ≥ 1, which is asserted as a
*producer*
property and enforced nowhere on the read side; out-of-vocabulary states
(the
harness can persist `"error"` as a no-data marker, which `rankOfState`
ranks above
red) and back-filled or externally-written rows both reach the
classifier at 0. A
`fail_count` of 0 on a red row is not positive evidence of a first
strike. The
golden master cannot see it: every red-bearing fixture uses
`fail_count: opts.failCount ?? (isRed ? 1 : 0)`, so no fixture puts a
red row at 0.
9. **An infra-red or `degraded` D1/D2 over a green ladder paints
chip=green,
achieved=6.** `combine.ts`'s §F gate fires only on `contribution ===
"FAIL_FRESH"`
in both the achieved-depth walk and `computeChip` step 1;
`INFRA_RED_FRESH` and
`DEGRADED` fall through as non-gating, so the cell keeps its green
ladder verdict
with `d6Effective: "green"`. Measured on the branch engine: `D1
infra-red
(driver-error) + green D2 → chip=green ach=6 reg=false d6Eff=green`, and
the same
for `D1 degraded`. **Mutation-proven invisible to the golden master:**
patching
*both* §F gate sites to also fire on `INFRA_RED_FRESH` and `DEGRADED` —
a
materially different verdict for every column whose health probe is
failing —
leaves **all 61 baseline entries byte-identical**. `health:<slug>` /
`agent:<slug>`
are integration-scoped, so this is whole-column. The polarity flip
closed the
`signal: undefined` hole here and left the `driver-error` / `abort` hole
wide open
— and `driver-error` / `abort` on a health probe is precisely what an
unreachable
container or a cancelled sweep produces.
# Known limitations of the guards themselves
Disclosed rather than omitted. These are properties of this PR's own
tests.
- **INV7's Q3 carve-out does not cover the M2 shape.** It is keyed on
`ladderGapDepth` and excuses only a gap strictly *below* the shallowest
red pill,
which is M1. The D6-outside-the-walk shape (M2, 2,048 combos) is a
different
mechanism and is not addressed by the carve-out.
- **The starter-axis INV7 canary short-circuits and proves nothing.**
`assertChipStripCoherent` returns immediately on `m.chipColor !== "gray"
||
redDepth === null`. The hard-red starter cell the test builds has
`chipColor ===
"red"`, so the function returns on the **first** disjunct — not on the
strip condition the comments claim. The call would pass identically if a
starter
cell *did* expose a red depth strip. The canary role is actually carried
by the
four `lvl.exists === false` / `lvl.status === null` assertions beside
it; the
`assertChipStripCoherent` call adds nothing. Compounding it,
`stripReadsRed` — the
identifier the comments reason about — does not exist anywhere in the
repo.
- **The five INV7 "teeth" assert `/INV7/`, which the drift-guard message
also
matches.** `assertChipStripCoherent` raises two structurally different
failures
and both messages contain the literal `INV7`: the keyspace-drift guard,
and the
real coherence failure. Every teeth test asserts only
`.toThrow(/INV7/)`. So if
`contributingKeys` ever drifts from the engine keyspace —
`collectAgentLadder`
gains a rung, `CATALOG_TO_D5_KEY` fan-out changes shape, a `keyFor`
segment changes
— `redRows` goes empty, the drift guard fires, and **all five teeth
still pass**
while INV7 has gone vacuous over the entire fixture matrix. That is
precisely the
state the drift guard was written to make impossible; it is silenced
inside the
only tests that reach it. The fix is to assert the specific message
(`/requires POSITIVE infra evidence/`) and pin the drift message in its
own case.
- **The test double cannot express absent-vs-null projection
provenance.**
`applyPbFields` silently drops projection tokens it does not model, so
the fake
cannot distinguish a key *omitted by a `fields=` projection* from a key
present
with value `null` — which is the single distinction `redSignalKnown`
rests on.
Fixtures therefore cannot validate the provenance semantics the fix is
built on;
that validation lives only in the live red-green capture above.
# Follow-ups
- **Epic: "A red must render red" — a single enforcement point for the
showcase
cell-model read model**, on Notion under CopilotKit *Plans / Proposals*:
https://app.notion.com/p/3a83aa38185281b99f7bfdd1d0f46bb9 — carries
M1–M3 and the
round-3 mechanisms above toward one enforcement point rather than nine
point-fixes.
- **`DEFERRED-r3.md` — 132 catalogued items** from review round 3
(comment accuracy,
test-double conformance, coverage gaps, altitude). Nothing in round 3
was
discarded; it was catalogued and routed.
# Quality gate
Re-run at head `b04330ab` in a clean detached worktree.
- **`oxfmt@0.36.0 --check`**: clean — "All matched files use the correct
format" (16
changed TS/TSX files)
- **`oxlint@1.51.0`** (repo `.oxlintrc.json`): **5 warnings, 0 errors.**
4 of the 5
reproduce verbatim on the `origin/main` versions of the same files
(`unicorn(consistent-function-scoping)` on `c`, `eslint(no-shadow)` on
`rows`,
`unicorn(no-array-reverse)`,
`typescript-eslint(consistent-type-imports)`). The one
net-new against `origin/main` is a second
`unicorn(consistent-function-scoping)` warning, on the test-local helper
`lvl` in
`cell-model.coherence.test.ts`. Zero errors; the repo's `oxlint` CI job
is green.
- **`tsc --noEmit`**: **0 errors** in `shell-dashboard`; `harness` clean
under
`tsconfig.build.json`. `harness`'s `tsc --noEmit` reports one
pre-existing error,
`frontend-matrix.test.ts` resolving
`../../../shell/src/data/frontend-catalog.json`
— a stale path in a file byte-unchanged from `origin/main`.
- **`shell-dashboard` tests** (measured locally at `b04330ab`): **71
files / 1407
passed / 1 skipped / 0 failed.** Scoped to `src/`;
`tests/runtime-env-switch.spike.test.ts` was deliberately not run (it
spawns
`next build` plus two `next start` invocations and is not a unit test).
- **`harness` tests** (measured locally at `b04330ab`): **3659 passed /
18 skipped /
3 failed** — the same three pre-existing D5 fixture-registry-drift
failures, in
files **byte-unchanged from `origin/main`** and driven entirely by
inputs none of
this PR's 17 files touch:
- `frontend showcase matrix > plans every runnable catalog cell without
loss or duplication` — `expected 664 to have a length of 660`
- `d5-mapping-drift > dashboard CATALOG_TO_D5_KEY structurally mirrors
harness REGISTRY_TO_D5` — the drift regex cannot locate
`CATALOG_TO_D5_KEY` in `shell-dashboard/src/lib/live-status.ts` (a
675-byte re-export shim on `main`, not modified here)
- `D5_REPRESENTATIVES > covers every D5FeatureType that has a registered
script` — missing `browser-use-smoke`
Those three are now **explicitly quarantined** by the `test /
unit-showcase`
workflow that landed on `main` in `7282ecddfb`
(`showcase/harness/vitest.quarantine.json`), and that workflow's
**quarantine
ratchet** re-runs each one to confirm it *still* fails — so they neither
hide nor
block. At this PR's head the ratchet passes: `Quarantine ratchet OK: all
3
quarantined file(s) still fail.`
- Diff hygiene: no lockfiles, no generated `src/data/*.json`.
## CI provenance — this is the PR's first honest CI
`main` has been **merged in** at `7282ecddfb` (merge commit `12ea621a`,
textually
clean, zero conflicts; the 13 commits above are unrewritten). This
matters for more
than being up to date: the `harness unit suite` and `shell-dashboard
unit suite`
jobs **did not exist** at `b04330ab`, because the workflow that enables
them landed
on `main` afterwards and GitHub runs `pull_request` workflows from the
PR head. The
earlier "17 pass" therefore ran **none** of the ~5,000 tests covering
this PR's own
subject matter (`cell-model/*` and `useLiveStatus*`).
- CI at this head (`12ea621a`): **21 pass / 0 pending / 0 fail** across
24 registered
rows.
- Both new jobs run and both pass — `shell-dashboard unit suite` **70
files / 1404
passed / 1 skipped**, `harness unit suite` green, quarantine ratchet OK.
- The **Q1–Q4 guard battery (16 re-proofs)** was re-run at the merge
head and
reproduced its pre-merge numbers exactly, so nothing was disarmed by the
merge:
Q1 cap→10 and cap→5 each **10 failed / 6 passed** (unmutated **16/16
green**);
Q2 M1–M6 = **3 / 1 / 2 / 6 / 5 / 6** (M4 flips the supplemental cap
test, M6 the
bulk cap test, M5 red in both worlds by design); Q3 `isStaleCell`
deletion = **1
failure** (the U8 force-gray case) and the engine `break`→`continue`
fires the
two-sided canary with `expected 'red' to be 'gray'`; Q4 each of the
three fold
mutations = **1 failed / 61 passed** via exactly one fixture
(`hetero-d4-red-failcount-max`, `hetero-starter-mixed-soft-hard`,
`hetero-d4-green-stripped-infra-red`), baseline key count **61**,
completeness
check present.
# Review guidance
**Surface: 17 files, +4880 / −382.** Most of that volume is tests,
fixtures, and the
regenerated golden-master baseline. Only three files carry production
behaviour:
`cell-model.contribution.ts`, `cell-model.ts`, and `useLiveStatus.ts`.
**The four round-2 units are separable commits** — `b8b75b7d` (Q2),
`75fdfb3a` +
`ed10b61f` (Q1), `49dea099` (Q3), `b04330ab` (Q4) — and round 1's are
too, so this
can be read unit by unit rather than as one diff.
**Highest-value review targets, in order:**
1. **`cell-model.contribution.ts` — the gate.** `if (fold.redSignalKnown
&&
!fold.hasNonInfraRed)`. This is the whole polarity decision. Worth
checking: that
`redSignalKnown` really is red-row-scoped inside `foldFamily` (a
family-wide
derivation is what made `INFRA_RED_FRESH` unreachable in the browser),
and that
the two conjuncts are genuinely independent evidence rather than one
restated.
2. **`useLiveStatus.ts` — `truncated` semantics.** Q1 redefined
`truncated` from
"cap exhausted" to "there are rows we did not read", proven by one
lookahead read
of page cap+1 whose rows are discarded and only whose emptiness is
load-bearing.
Worth checking: the asymmetry (bulk throws, supplemental warns) is
applied at the
right call sites, and that the lookahead genuinely cannot be confused
with a data
page. Note limitation 5 above — this contract does **not** cover
wave-level loss.
Everything else is fixtures, doubles, and the coherence suite; the
disclosed
weaknesses in those are listed under *Known limitations of the guards
themselves*
rather than left for a reviewer to rediscover.
|
||
|
|
5bf4b6f74f |
ci(showcase): pull demo-file LFS objects in the Python unit-test job
PR #6163 converted showcase/integrations/*/public/demo-files/* to Git LFS. showcase_validate.yml's python-unit-tests job checks out without LFS, so the working tree holds 129-byte pointer text instead of the real assets. The new ms-agent-python multimodal test is the first Python test to read those bytes: pypdf reads the pointer, extracts nothing, and the test fails with "real pypdf text extraction produced nothing". Fetch only the demo-file assets (40 objects, ~320 KB) rather than setting lfs: true, which would pull all ~248 tracked objects (~475 MB, including 13-26 MB README gifs) into a 2-4 minute job under a 10 minute cap. A job that hits timeout-minutes reports 'cancelled' -- neither success nor failure -- and silently suppresses alerting, so the timeout margin is worth protecting. The repo is public, so LFS downloads resolve anonymously and the pull works with persist-credentials: false. A post-pull %PDF- header check fails the step immediately if the pull ever no-ops, instead of surfacing minutes later as a misleading pytest assertion. |
||
|
|
db75a04837 |
chore(showcase): mark multimodal unsupported for llamaindex and crewai-crews (#6158)
## What this is
`multimodal` (Attachments) has never worked on **`llamaindex`** or
**`crewai-crews`**, but both manifests
listed it under `features`, so the fleet probed it and reported **red**.
A red chip says "this regressed".
The truth is "this was never built". This PR marks both cells
**unsupported** instead. It does **not**
implement the feature, and it does **not** suppress the cell — the cell
still exists, the demo stays wired,
and the chip renders the 🚫 unsupported glyph.
## Mechanism used (existing, not invented)
The repo already has exactly one way to declare a feature unsupported
for an integration:
**`not_supported_features` in the integration's `manifest.yaml`**. The
full derivation chain:
| Step | Location |
|---|---|
| Declaration | `showcase/integrations/<slug>/manifest.yaml` ->
`not_supported_features:` |
| Schema | `showcase/shared/manifest.schema.json` — *"feature IDs that
this integration's framework cannot architecturally support … excluded
from parity computation"* |
| Status fold |
`showcase/harness/src/shared/catalog/catalog-flatten.ts:239` —
`determineCellStatus()` checks `not_supported_features` **first**,
returns `status: "unsupported"` |
| Input mapping |
`showcase/harness/src/shared/cell-model/catalog-input.ts:53` —
`isSupported: cell.status !== "unsupported"` |
| Model | `showcase/harness/src/shared/cell-model/cell-model.ts:847` —
`if (!isSupported) return UNSUPPORTED;` (the frozen singleton at `:551`:
`supported: false`, `chipColor: "gray"`, `isRegression: false`) |
| `/api/matrix` | `showcase/harness/src/http/matrix.ts:206` ->
`matrix-compute.ts:53` — projects that same model, so the API value
**is** the rendered chip by construction |
| Render |
`showcase/shell-dashboard/src/components/unified-cell.tsx:308` — `if
(!model.supported)` renders `data-testid="unified-cell-unsupported"`
with 🚫 and `title="Not supported by this framework"` |
The mechanical guard at `catalog-flatten.ts:169` rejects a feature that
appears in **both** `features` and
`not_supported_features`, so each entry was **moved**, not duplicated.
Note this mechanism is strictly stronger than a probe-side skip:
`buildCellModel` returns `UNSUPPORTED`
regardless of what the live PocketBase row says. Verified against the
existing not-supported cells on these
same two integrations, which carry **green** PB rows and still render 🚫:
```
llamaindex/gen-ui-interrupt matrix: chip=gray supported=False | PB rows: d5=green d6=green e2e=green
llamaindex/shared-state-streaming matrix: chip=gray supported=False | PB rows: d5=green d6=green e2e=green
crewai-crews/mcp-apps matrix: chip=gray supported=False | PB rows: d5=green d6=green e2e=green
```
So the cell can never read green *or* red once declared here — which is
the property we want.
## Per-integration reason (recorded inline in each manifest)
**`llamaindex` — upstream gap.** The pinned
`llama-index-protocols-ag-ui==0.2.2`
(`llama_index/protocols/ag_ui/utils.py:82-85`) passes an AG-UI
`UserMessage`'s `content` straight into
`ChatMessage(...)`. A text-only turn passes a plain string (fine — every
other llamaindex cell is green);
an attachment turn passes a **list** of AG-UI content-part models, which
pydantic routes into
`ChatMessage.blocks`, a union discriminated on `block_type` — a field
AG-UI's `TextInputContent` /
`ImageInputContent` / `BinaryInputContent` do not carry. Live backend
error:
```
pydantic_core._pydantic_core.ValidationError: 3 validation errors for ChatMessage
blocks.0
Unable to extract tag using discriminator 'block_type' [type=union_tag_not_found,
input_value=TextInputContent(type='te... image I just attached'), input_type=TextInputContent]
```
Fixing it needs a content-part ->
`TextBlock`/`ImageBlock`/`DocumentBlock` conversion, upstream or on our
side of `get_ag_ui_workflow_router`. This PR also corrects that module's
docstring, which asserted the
router *"normalizes them via the OpenAI `input_file` path"* — it does
not.
**`crewai-crews` — never implemented, ours.** `src/agent_server.py`
registers a dedicated AG-UI endpoint for
every other demo but **no `/multimodal` route** (the block ends at the
catch-all
`add_crewai_crew_fastapi_endpoint(app, LatestAiDevelopment(), "/")`),
and there is no
`src/agents/multimodal_agent.py`. So
`src/app/api/copilotkit-multimodal/route.ts` aliases the generic shared
crew, which has no vision handling and dies on a content-part message:
```
[CopilotKit] Error (agent_run_error_event): Error: thread=… run=…: CrewAI flow failed; see server logs
```
That route file's own header comment already concedes the gap ("A
dedicated per-demo crew with vision-tuned
agent prompts is tracked as follow-up work").
## Proof
Method: the **real** `GET /api/matrix` handler (`registerMatrixRoute`)
driven over the **real** production
PocketBase `status` collection (all 3082 rows, fetched verbatim from
`showcase-pocketbase-production.up.railway.app`) and the **real**
on-disk manifests (default `loadCells` =
`buildCatalogCells`, the single flattening authority). Same rows, same
fixed clock
(`now = max(observed_at) + 60s = 1784932566438`) for both runs — the
only variable is the manifest diff.
### BEFORE — real `GET /api/matrix`, real prod PocketBase rows,
manifests at `origin/main` (
|
||
|
|
59f275eedc |
fix(showcase/mastra): restore shared-tools symlink, shrink erosion ratchet (#6161)
## What
`showcase/integrations/mastra/shared-tools` was a **real committed
directory** where a **symlink into the single source of truth** belongs.
This restores the symlink (`-> ../../shared/typescript/tools`) and
shrinks the erosion ratchet accordingly.
This is the iron-rule violation described in `showcase/AGENTS.md` → "The
single-source symlink mechanism": `*/shared-tools`, `*/tools`, and
`*/_shared` are meant to be symlinks into `showcase/shared/...`, so
edits to the shared source silently do NOT reach an integration whose
symlink has been clobbered by a real directory.
## Root cause
| | |
|---|---|
| Symlink originally added | `93d5815cdb` — *"refactor: add shared tools
symlink to mastra, update path alias"* (target
`../../shared/typescript/tools`) |
| Symlink clobbered | `534cd1efa7` — *"fix(showcase): D5 integration
fixes across 12 frameworks"* — deleted the symlink (`shared-tools \| 1
-`) and committed **14 real files** in its place |
That commit clobbered `shared-tools` for **three** TS integrations at
once: `mastra`, `langgraph-typescript`, and `claude-sdk-typescript` —
all three grandfathered in
`showcase/scripts/validate-shared-symlinks.baseline.json`.
mastra's Dockerfile already documented the symlink as the expected
state, so the tree had drifted from its own documented contract:
```
# shared-tools/ is a symlink to ../../shared/typescript/tools — resolved at
# build time by CI which copies the target into the build context.
```
## Divergence inventory — **NONE**
Diffed the real directory against `showcase/shared/typescript/tools`
exhaustively **before** changing anything. **Zero divergence**:
identical file set, identical git blob hashes, identical sha256s across
all 14 files.
| File | git blob (main) | classification |
|---|---|---|
| `__tests__/generate-a2ui.test.ts` | `b3c8223e` | identical copy |
| `__tests__/get-weather.test.ts` | `57dd5b94` | identical copy |
| `__tests__/query-data.test.ts` | `ef284b0b` | identical copy |
| `__tests__/sales-todos.test.ts` | `8c5c0d6e` | identical copy |
| `__tests__/schedule-meeting.test.ts` | `ab470b3b` | identical copy |
| `__tests__/search-flights.test.ts` | `f64b8782` | identical copy |
| `generate-a2ui.ts` | `7d76c00c` | identical copy |
| `get-weather.ts` | `0b444f7a` | identical copy |
| `index.ts` | `3b0e2a45` | identical copy |
| `query-data.ts` | `48a2cbdc` | identical copy |
| `sales-todos.ts` | `96b1874f` | identical copy |
| `schedule-meeting.ts` | `d5e626e8` | identical copy |
| `search-flights.ts` | `7a943023` | identical copy |
| `types.ts` | `f87f26d1` | identical copy |
Classification totals:
- **stale** (shared moved on, mastra behind): none
- **mastra-specific** (intentional or accidental local edit): **none**
- **additive** (file only in mastra's copy): none
- files present in one and not the other: **none**
```
$ git diff --no-index --stat integrations/mastra/shared-tools shared/typescript/tools
(no output — identical)
```
Because the divergence set is empty, restoring the symlink is a **pure
structural fix with zero content change** — nothing mastra-specific
existed in the copy, so nothing can be silently lost. The restored link
blob is `ddc634b6` — **the same git object** the pre-erosion symlink
had.
## AFTER — structural proof
```
$ ls -la showcase/integrations/mastra/shared-tools
lrwxr-xr-x shared-tools -> ../../shared/typescript/tools
$ git ls-files -s showcase/integrations/mastra/shared-tools
120000
|
||
|
|
7b28934387 |
fix(showcase): use each integration's own name in demo page titles (#6162)
## What `showcase/integrations/*/src/app/demos/layout.tsx` was cloned from `langgraph-python` and 14 copies kept its hardcoded `"LangChain - Python"` title. Every `/demos/*` page in those integrations rendered someone else's name in the browser tab. Each file now uses the display name from its own `manifest.yaml` `name:` field. Two string literals per file, nothing else. ## Convention matched The already-correct integrations hardcode their own display name, which equals their `manifest.yaml` `name:`: - `langgraph-typescript` → `LangGraph (TypeScript)` - `strands-typescript` → `AWS Strands (TypeScript)` Same shape used here: bare name when no demo slug is resolvable, `"<Name> - <Demo>"` otherwise. ## Full list of mismatched titles (all fixed) | Integration | Before | After | | --- | --- | --- | | `ag2` | LangChain - Python | AG2 | | `agno` | LangChain - Python | Agno | | `built-in-agent` | LangChain - Python | CopilotKit's Built-in Agent | | `claude-sdk-python` | LangChain - Python | Claude Agent SDK (Python) | | `claude-sdk-typescript` | LangChain - Python | Claude Agent SDK (TypeScript) | | `crewai-crews` | LangChain - Python | CrewAI (Crews) | | `langgraph-fastapi` | LangChain - Python | LangGraph (FastAPI) | | `langgraph-python` | LangChain - Python | LangGraph (Python) | | `langroid` | LangChain - Python | Langroid | | `llamaindex` | LangChain - Python | LlamaIndex | | `mastra` | LangChain - Python | Mastra | | `pydantic-ai` | LangChain - Python | PydanticAI | | `spring-ai` | LangChain - Python | Spring AI | | `strands` | LangChain - Python | AWS Strands (Python) | `langgraph-python` is included because `"LangChain - Python"` is the legacy Notion partner-column label, not the integration's name — its own `manifest.yaml` and root layout both say `LangGraph (Python)`. Untouched (already name themselves correctly): `langgraph-typescript`, `strands-typescript`, `ms-agent-dotnet`, `ms-agent-harness-dotnet`, `ms-agent-python`. ## Proof — rendered titles from locally served pages Not a source diff: `next dev` per app, real `document.title` read via Playwright / served HTML. Red → green on the same running server (`mastra`, `/demos/multimodal`, port 3411): ``` RED mastra DOM document.title = "LangChain - Python" <- HEAD version of the file GREEN mastra DOM document.title = "Mastra" <- with this change ``` `langgraph-python` (`/demos/multimodal`, port 3415) — the only integration whose middleware sets `x-pathname`, so it is the one that exercises the `"<Name> - <Demo>"` branch: ``` BEFORE <title>LangChain - Python - Attachments</title> AFTER <title>LangGraph (Python) - Attachments</title> ``` Second sample, `strands` (`/demos/multimodal`, port 3414): ``` BEFORE <title>LangChain - Python</title> AFTER <title>AWS Strands (Python)</title> ``` Control — `langgraph-typescript` (`/demos/multimodal`, port 3413), file not touched: ``` BEFORE <title>LangGraph (TypeScript)</title> AFTER <title>LangGraph (TypeScript)</title> (unchanged) ``` ## Why the title is not read from `manifest.yaml` at runtime The obvious "right level" fix is to have `generateMetadata` read `manifest.name` instead of a literal. I built that, then backed it out: **15 of 20 runner images never copy `manifest.yaml`**, so a `manifest.yaml` read on the no-slug path (which is the path every integration except `langgraph-python` actually takes, since only that one has middleware setting `x-pathname`) would throw ENOENT and 500 **every** demo page in production. `langgraph-python`'s Dockerfile documents exactly this failure mode at the line where it copies the manifest in. Making the config the runtime source would mean touching 15 Dockerfiles and re-verifying 15 image builds — out of proportion for a title string, and a much larger blast radius. ## Adjacent issues found, deliberately not fixed here 1. **`x-pathname` is only set by `langgraph-python`** (`src/middleware.ts`). Everywhere else `generateMetadata` never resolves a slug, so demo pages show just the integration name and never `"<Name> - <Demo>"`. The per-demo title needs middleware per app (or a different pathname source) — separate change. 2. **`google-adk` has no `src/app/demos/layout.tsx`** at all, so its demo pages fall back to the root layout's `"CopilotKit Showcase"`. That is a missing file, not a wrong string. 3. **Root `src/app/layout.tsx` titles are inconsistent** — 16 integrations use a bare `"CopilotKit Showcase"` while `mastra`, `langgraph-python`, `langgraph-typescript` and `built-in-agent` append their own name. No integration names a *different* integration there, so it is untidy rather than wrong. ## Checks - `prettier --check` on all 14 changed files: clean - `tsc --noEmit` in `mastra` (107 pre-existing errors, unrelated files) — count identical with the HEAD version of the file vs this change; no errors in `demos/layout.tsx`. Same for `strands` and `langgraph-typescript`. - No test references these titles (`shell-dashboard`'s `"LangChain - Python"` strings are the Notion partner label and are untouched), so there are no affected tests. |
||
|
|
c2e9264dde | Merge branch 'main' into fix/ms-agent-python-multimodal-prompt | ||
|
|
4a303f8bed | Merge branch 'main' into chore/multimodal-unsupported-llamaindex-crewai | ||
|
|
2825b10ba1 | Merge branch 'main' into fix/mastra-shared-tools-symlink | ||
|
|
b907a2660a | Merge branch 'main' into fix/showcase-integration-page-titles | ||
|
|
d28384a2eb |
fix(showcase/ci): make a partially-cancelled build loud, and verify what it actually shipped (#6171)
## The hole A partially-cancelled showcase build was **indistinguishable from a clean one in both directions**: it emitted no alert, and it suppressed post-deploy verification — while having actually redeployed real images to staging. Three merges landed on `main` on 2026-07-25 with **zero post-deploy verification**. Reconstructed from the API, with run IDs: | # | build run | conclusion | what happened | |---|---|---|---| | #5483 | `30162754730` | cancelled | `shell-docs` killed at 10m04s. Alert **did** fire (all-failed path). No verify. | | — | `30162770765` | cancelled | `shell` killed at 10m04s. **Silent.** No verify. | | #6160 | `30162773601` | cancelled | 5 slots killed, **23 redeployed to staging**. **Silent.** No verify. | | #6168 | `30162784491` | success | clean — and its verify was then cancelled by a sibling. | ### The chain, for run `30162773601` 1. #6160's workflow edit forced a full-fleet rebuild. Five slots died — `shell`, `shell-docs`, `shell-dashboard`, `shell-dojo`, `showcase-aimock`. 2. The intersection guard correctly excluded them. `redeploy-staging` **succeeded** and uploaded `redeploy-summary`, redeploying the 23 that built. 3. The run concluded **`cancelled`** (5 legs cancelled, 0 failed → GitHub rolls the run up to `cancelled`). 4. `notify` was **skipped** → no Slack, no PR comment. 5. `showcase_deploy.yml` requires `conclusion == 'success'` → never started. 23 services redeployed, unverified. 6. The one legitimate verify — run `30163309977`, from #6168's successful build — started 15:17:13 and was **cancelled at 15:17:22** by run `30163312882`, which was triggered by a *different* partially-cancelled build and then skipped every job anyway. ## Which links I verified in the YAML, and where the brief was wrong | Link | Verdict | |---|---| | Intersection guard excludes cancelled slots | ✅ Confirmed — `showcase_build.yml` `redeploy-staging` intersects matrix ∩ `status == "success"`. | | Run concluded `cancelled`, not failure/success | ✅ Confirmed via API on all three runs. | | `showcase_deploy.yml` requires `conclusion == 'success'` | ✅ Confirmed verbatim on `resolve-matrix`. | | Global `showcase-verify-deploy` concurrency group preempted a sibling | ✅ Confirmed — group had no commit key, `cancel-in-progress: true`; timestamps above show the 9-second preemption. | | **`notify`'s condition is `if: failure()`** | ❌ **Wrong.** It is already `!cancelled() && (failure() \|\| any_success == 'false')`. | | **The suppressor is `failure()` not matching `cancelled`** | ❌ **Wrong, and the proposed fix would not have worked.** See below. | | **Depot `Step canceled by GitHub` is a flake** | ❌ **Wrong.** It is a `timeout-minutes` kill. See "Root cause". | ### Why `if: failure() || cancelled()` would NOT have fixed this I built a throwaway probe workflow on a scratch branch — a matrix with one leg killed by `timeout-minutes` and one green leg, plus dependent jobs guarded by each candidate expression. **Probe run [`30166429073`](https://github.com/CopilotKit/CopilotKit/actions/runs/30166429073)**: | probe | result | conclusion | |---|---|---| | killed leg's `job.status` | `cancelled` | a timeout kill reports as *cancelled* | | matrix rollup `needs.legs.result` | `cancelled` | | | `if: cancelled()` | **SKIPPED** | `cancelled()` evaluated **FALSE** | | `if: failure()` | **SKIPPED** | `failure()` evaluated **FALSE** | | pre-fix `notify` condition | **SKIPPED** | ← the bug, reproduced | | post-fix `notify` condition | **RAN** | ← the fix, proven | | workflow run conclusion | `cancelled` | matches production | `cancelled()` is **workflow-scoped**: ["Returns `true` if the workflow was cancelled."](https://docs.github.com/en/actions/reference/evaluate-expressions-in-workflows-and-actions) Individual legs dying does not cancel the *run*, so it is false. And `failure()` ["returns `true` if any ancestor job fails"](https://docs.github.com/en/actions/reference/evaluate-expressions-in-workflows-and-actions) — a **cancelled** ancestor did not *fail*, so it is false too. With 23 slots green, `any_success` is `'true'`. Every clause the old guard had went the wrong way. Independently corroborated in production: in run `30162773601`, both `!cancelled()`-guarded jobs (`aggregate-build-results`, `redeploy-staging`) **ran**, which is only possible if `cancelled()` was false. ## Root cause of the cancellations: a timeout, not a flake From the Depot log of the killed `shell` leg: ``` 15:04:03 depot build --file showcase/shell/Dockerfile ... --push 15:13:29 #25 [builder 15/15] RUN cd scripts && ... generate-registry.ts DONE 54.3s 15:13:32 failed to solve: Canceled: context canceled 15:13:32 ##[error]Step canceled by GitHub 15:13:32 BUILD_STATUS: cancelled ``` Started 15:03:28, killed 15:13:34 — **exactly `timeout-minutes: 10`**. Same for the other three shell slots; `showcase-aimock` died at exactly its 5. The build was making real but slow progress the whole time (repeated docker.io base-image pull stalls, `#9 ...` for seven minutes) and was 2 steps from done. So **a retry is the wrong lever twice over**: a cancelled job cannot run further steps, and the build was not erroring. The underlying cause is Depot builder contention — three full-fleet rebuilds ran concurrently (~84 simultaneous amd64 builds). Measured budgets: | slot | warm | contended | budget | |---|---|---|---| | shell / shell-docs / shell-dashboard / shell-dojo | 2–5 min | killed at 10 | **10 → 20** | | showcase-aimock | 2 min | killed at 5 | **5 → 12** | | showcase-pocketbase | — | **8.8 min** (near-miss) | **10 → 15** | | webhooks | 0.6 min | — | 5 (unchanged, `skip_build`) | The `context: "."` slots build the whole monorepo root and are the heaviest in the fleet, yet carried the *smallest* budgets — inverted. ## The design **1. Stop laundering the signal** (`build-outputs.ts`, per-slot writer). `cancelled` becomes a first-class `BuildOutcome` instead of being mapped to `skipped`. The per-slot result is the *only* place the cancellation survives, because the status functions can't see it. `successSet` still excludes it, so the redeploy intersection is unchanged — a slot that pushed no image still cannot enter the redeploy CSV. **2. Alert on it** — aggregator publishes `any_cancelled` + `cancelled_services`; `notify` gains an `any_cancelled == 'true'` clause (the exact expression proven by the probe), and its PR comment now distinguishes *incomplete* from *failed*. **3. Red the run, don't leave it `cancelled`** — new `notify-cancelled-builds` job exits non-zero, so the conclusion is `failure`. A slot killed by its timeout budget *is* a failure, and `cancelled` is precisely what suppressed everything downstream. **4. Verify the partial deploy anyway** — the `conclusion == 'success'` gate becomes a terminal-conclusion allowlist (`success|failure|cancelled|timed_out`). > **Tradeoff, stated plainly.** Relaxing the gate rather than only failing loudly is deliberate: a build that redeployed 23 services *needs* verifying, and gating on `success` guaranteed it never would. It is safe because the decision of *what* to verify never depended on the rollup — no `redeploy-summary` artifact still means `has_services=false` and verify is skipped, and the redeploy gate still narrows to the per-service success set, so a cancelled slot can never be probed against a stale `:latest` and reported healthy. The cost is one extra no-op deploy run for builds that die before redeploying. Note this *also* fixes the same hole for `conclusion == 'failure'`, which was equally unverified. **5. Key verification per commit** — `showcase-verify-deploy-${{ github.event.workflow_run.head_sha || github.sha }}`, so a later cancelled run can no longer destroy an earlier successful run's verification. `cancel-in-progress: true` is kept, so re-verifying the *same* commit still supersedes. (`github` context is [documented as available](https://docs.github.com/en/actions/reference/workflows-and-actions/contexts) in `concurrency`.) **6. Realistic timeout budgets** — separate commit, droppable independently. Mitigation, not the root fix; the durable lever is a non-cancelling concurrency queue on the build workflow, noted inline as a follow-up since this workflow deliberately has *no* concurrency group. ### On alerting for a genuine manual cancel It stays **silent**, and that is correct. `!cancelled()` is retained on both notify jobs as the flake-vs-intentional discriminator, and the probe proves it discriminates: a human cancelling the whole RUN makes `cancelled()` true → silent; a leg-level timeout leaves it false → alerts. Belt and braces — on a run-level cancel, `aggregate-build-results` is itself skipped by its own `!cancelled()` guard, so `any_cancelled` resolves to `''` rather than `'true'`. Two independent reasons for silence, zero manual-cancel noise. ## What I proved - **Real end-to-end red-green on GitHub Actions** — probe run [`30166429073`](https://github.com/CopilotKit/CopilotKit/actions/runs/30166429073), table above. Old condition skipped, new condition ran, on a genuinely timeout-killed matrix leg. Scratch branch deleted; the run is permanent. - **Local red-green on the TS layer** — 8 tests failing before the fix (`cancelledSet is not a function`, `invalid "status" (must be success|failure|skipped)`), 46 passing after. - **Local red-green on the live workflow expression.** `redeploy-guard.test.ts` parses the **actual `if:` strings out of the YAML** and evaluates them. Pointed at `origin/main`'s YAML, the new `GREEN: the live guard FIRES on the production partial-cancel` test fails (`expected false to be true`) and is the *only* failure; pointed at this branch, all 31 pass. The pre-fix guard is pinned as a literal so the test permanently encodes the difference rather than just the current behaviour. - **No new alert noise** — explicit tests that `notify` stays silent on a clean build, on a human run-cancel, and on a no-changes push. That last one exposed a latent inaccuracy in the test harness's own model (a skipped aggregator's outputs are `''`, not `'false'`), now fixed. - **Lint** — `actionlint` finding count unchanged at 10 vs the `origin/main` baseline (all pre-existing: the unknown `depot-ubuntu-24.04-4` label and shellcheck `info` notes on untouched scripts). `zizmor` with the repo's own config at `min-severity: low`: *No findings to report*. - **Suite** — 77 tests green across the three touched files. Full `showcase/scripts` suite has 5 pre-existing failing files (`emit-railway-envs-json`, `generate-catalog`, `generate-registry`, `integration-smoke-registry`); I reproduced the identical 5 files / 8 tests on a pristine `origin/main` worktree, so they are unrelated to this change. ## What remains unproven until it runs on `main` - The **production** partial-cancel path end-to-end. The probe reproduced the expression semantics on a synthetic matrix, not the real 28-slot fleet with real Depot builds; the Slack message and PR-comment rendering have not been exercised against a live webhook. - The relaxed deploy gate firing on a real `cancelled`/`failure` build — including the "no artifact → skipped, no noise" path. - The per-commit concurrency group under real rapid-fire merges. - Whether 20 minutes is actually enough for `shell` under peak Depot contention. It was ~3 steps from done at 10; 20 is inference from that, not measurement. `\n` handling in the new Slack payload follows the existing `fromJSON('"\n"')` convention in this file, but is not verified against a live webhook. |
||
|
|
0c38124799 |
fix(react-core): stop CopilotPopup remounting chat on resize
CopilotPopup built its `chatView` override inside a `useMemo` keyed on `width`/`height`. Consumers driving those props from a drag-to-resize handle (committing new dimensions on mouseup) minted a new component function per resize; rendering a new element type at that slot makes React unmount and remount the whole chat subtree. The remount resets scrollTop to 0, then `initial="smooth"` re-animates the message list top-to-bottom on every resize, from any scroll position. Give the override a stable module-scope identity and pass the popup shell props (header, toggle, width, height, clickOutsideToClose, defaultOpen) through React context. Resizing is now a plain style update on CopilotPopupView with no remount, so scroll position holds. Add a regression test asserting the chat subtree stays mounted (mount count stays at 1) across width/height changes: it fails on the prior code (one extra mount per resize) and passes with the fix. |
||
|
|
b065664bc9 |
fix(showcase/ci): give the monorepo-root build slots a realistic timeout budget
Root cause of the cancellations, from the Depot logs of the killed slots: they were not a transient flake. Every one died at EXACTLY its `timeout-minutes` value (shell/shell-docs/shell-dashboard/shell-dojo at 10m04s of a 10-minute budget; showcase-aimock at 5 minutes), and the log ends with `failed to solve: Canceled: context canceled` followed by Depot's `Step canceled by GitHub`. GitHub reports a `timeout-minutes` kill as job conclusion `cancelled`, which is what fed the whole silent chain. The builds were making real but slow progress the entire time — repeated docker.io base-image pull stalls, and the final `generate-registry` layer alone took 54s — and shell was still 2 steps from done when the budget killed it. The `context: "."` slots build the whole monorepo root and are the heaviest in the fleet, yet they carried the SMALLEST budgets (10 min vs 15 for each small per-integration build). Measured, same slots: warm / uncontended ......... 2-5 min contended, killed at ....... 10 min (runs 30162773601, 30162770765) showcase-pocketbase ........ 8.8 min of a 10 min budget (near-miss) So a retry is the wrong lever twice over: a cancelled job cannot run further steps, and the build was not erroring. Give the four shell slots 20, showcase-aimock 12, and showcase-pocketbase 15. `webhooks` stays at 5 (it is skip_build, measured at 0.6 min). This is mitigation, not the root fix — the underlying cause is Depot builder contention from three concurrent full-fleet rebuilds (~84 simultaneous amd64 builds). Noted inline as a follow-up, since the durable lever is a non-cancelling concurrency queue and this workflow deliberately has no concurrency group. |
||
|
|
2e85bfab08 |
fix(showcase/ci): verify partial deploys and stop cross-commit verify preemption
Two independent ways verification was silently lost on 2026-07-25.
1. `resolve-matrix` required `github.event.workflow_run.conclusion ==
'success'`. But `redeploy-staging` gates on the artifact-derived
`any_success`, not on the matrix rollup, so a build with some slots
cancelled (rollup `cancelled`) or failed (rollup `failure`) still
pushes real images and still redeploys the slots that built. Run
30162773601 redeployed 23 services to staging and this workflow never
started. An unverified real deploy is worse than a verified partial
one, so the trigger is now "the build reached a terminal conclusion"
and WHAT to verify stays decided by evidence: no redeploy-summary
artifact still means has_services=false and verify is skipped, and the
redeploy gate still narrows to the per-service success set, so a
cancelled slot can never be probed against a stale `:latest`.
2. The `showcase-verify-deploy` concurrency group was global, so any
later-finishing build preempted an earlier run's verification even
though they verify DIFFERENT commits. Verify run 30163309977 for the
one genuinely successful build of the day (#6168,
|