mirror of
https://github.com/CopilotKit/CopilotKit.git
synced 2026-09-14 16:26:20 +08:00
fix-nextjs-copilotchat-error
14262 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
88fc3da497 | fix(next): guard root bodies against extension hydration races | ||
|
|
87a33ee6b8 |
fix(runtime): bound the in-memory agent runner to prevent OOM (#5837)
## Problem
The OSS/SSE runtime path — `CopilotRuntime`/`CopilotSseRuntime` built
**without** an Intelligence backend — uses `InMemoryAgentRunner`, which
kept all thread history in a process-global `Map` that nothing ever
bounded. A production Node.js deployment (`--max-old-space-size=4096`)
hit a fatal V8 OOM after ~173 threads / ~15.5k runs. Three unbounded
growth vectors: thread count, runs per thread, and per-run
`ReplaySubject(Infinity)` buffers.
## Fix
Make the in-memory runner **safe by default**, zero-config and
dependency-free.
- **`ɵBoundedThreadStore`** owns the `Map`, LRU ordering, approximate
byte accounting, and eviction. `InMemoryAgentRunner` keeps all streaming
logic and delegates storage.
- **Three bounds, whichever trips first:** `maxThreads` (**1000**, LRU),
`maxRunsPerThread` (**100**, FIFO; `Infinity`/`0` disables), `maxBytes`
(**512 MiB**, the primary guard).
- **Never evicts** a running thread, one still finalizing after
`stop()`, or the thread currently being created/appended — it accepts
temporary overage instead.
- **Thread-level `messagesSnapshot` and `createdAt`**, decoupled from
`historicRuns`, so run-cap eviction can neither drop the message history
nor drift a thread's reported creation time.
- **Subject-buffer release:** `ReplaySubject(Infinity)` is retained only
while a run is active, released on completion once events are in
`historicRuns`.
- **Both eviction paths log**, through one warn-once-per-store latch:
whole-thread eviction *and* per-thread run-cap trimming.
- **Limits validation:** `ɵnormalizeLimits` rejects negative / `NaN` /
non-integer / `-Infinity` values at construction and at `setLimits`,
clamping to defaults with a warning. Previously `maxRunsPerThread: -1`
crashed with a `TypeError` inside a fire-and-forget path.
- **Partial limit updates preserve siblings** — tuning one bound no
longer silently resets the others process-wide.
## API
Additive and non-breaking. Bounds ride on the existing options bag
introduced by `onConcurrentRun`:
```ts
export interface InMemoryAgentRunnerOptions extends InMemoryLimits {
onConcurrentRun?: "throw" | "supersede";
}
new InMemoryAgentRunner(); // unchanged, safe defaults
new InMemoryAgentRunner({ onConcurrentRun: "supersede" }); // unchanged
new InMemoryAgentRunner({ maxThreads: 200, maxBytes: 128 * 1024 ** 2 });
```
Note the differing scopes, documented on both surfaces: the limits
reconfigure the **process-global** store; `onConcurrentRun` is
**per-runner**. Passing only `onConcurrentRun` leaves limits untouched.
## Interaction with `onConcurrentRun: "supersede"`
Rebased onto `main` after supersede landed, which surfaced a bug neither
change had alone. A superseded run's teardown was not isolated from the
run that replaced it: the new run cleared the shared `stopRequested`, so
the superseded run finalized as a synthetic `RUN_ERROR`, and the
`prevSubject` bridge forwarded that terminal event — plus a replayed
`RUN_STARTED` — into the **live** run's subject. `connect()` consumers
replayed a duplicate `RUN_STARTED` followed by a dangling `RUN_ERROR`.
Fixed by capturing per-run finalize intent (`RunFinalizeControl`),
widening the concurrency gate to `isRunning || stopRequested`, removing
the leaking bridge, and identity-guarding the subject release. Both
teardown paths were extracted into one shared `finalizeRun` helper so
they cannot diverge again.
## Docs
Neither constructor option was documented before this PR.
- `backend/agent-runner.mdx` — the bounds and their defaults, the
**eviction model stated once** (whole-thread eviction removes the thread
from `GET /threads`; run-cap trimming keeps it, preserving `createdAt`
and the message snapshot; the warning is once-per-store, reset by
`clearThreads()`), the `onConcurrentRun` modes, and durability options
including the first-party `SqliteAgentRunner`.
- `troubleshooting/common-issues.mdx` — growing runtime memory and the
eviction warning, cross-linked both ways.
- Runtime skill references refreshed — they still described a removed
`globalThis`-keyed store with HMR backup — and their source citations
re-anchored on stable symbols rather than line numbers.
## Tests
**1939 passing, 0 skipped** across 133 files (`@copilotkit/runtime`);
`check-types`, `build`, `publint`, and `attw` all clean.
- `ɵBoundedThreadStore` units: thread-LRU, run-cap FIFO + opt-out,
byte-ceiling eviction, never-evict-running, never-evict-finalizing (both
count and byte paths), snapshot dedup, `createdAt` stability past the
run cap, warn-once, `clear()` reset.
- Limits validation: every field × every invalid shape, plus proof the
`0`/`Infinity` sentinels still disable the run cap.
- Supersede/stop: teardown isolation with **negative** assertions (no
phantom `RUN_ERROR`, exactly one `RUN_STARTED` for a `connect()`
subscriber on a superseded thread), plus `stop()`'s reachable rollback
branches.
- **OOM guard:** drives a sustained workload across the thread-count,
runs-per-thread, and byte bounds and asserts eviction through the
runner's public surface. It replaces an earlier heap-plateau test that
was both `--expose-gc`-gated (so it never ran in normal CI) and
non-discriminating (its workload crossed no bound, so it passed with
bounding removed). The replacement was verified to fail when
`enforceRunCap` is neutered.
## Caveats & follow-ups
1. **Non-durable by design.** Eviction is history loss, same as a
restart. The durable paths are `SqliteAgentRunner` (single instance) or
the Intelligence runner.
2. **Store stays process-global.** Pre-existing cross-endpoint bleed and
process-wide `clearThreads()` are unchanged; instance-scoping is a
follow-up needing maintainer sign-off.
3. **Multiple runners with differing limits → last-constructed wins**,
with a one-time warning. Partial updates now preserve unspecified
bounds.
4. **Byte accounting is approximate** (serialized-length estimate), not
exact heap bytes, and under-counts non-serializable payloads.
5. **Behavioral change to a default path.** History that previously
persisted until OOM now evicts past the limits — shipped as a `fix`
(crash → bounded), observable at scale. Run-cap trimming now also logs
once per store.
6. **`maxBytes` is a cross-thread ceiling.** It evicts other LRU threads
and never trims the just-appended thread, so a single hot thread is
bounded by `maxRunsPerThread` only — which is why the docs steer away
from disabling that cap.
7. **Dedup weakens past the run cap.** `historicMessageIds` is rebuilt
from `historicRuns`, so a thread beyond `maxRunsPerThread` can
re-present already-seen messages. Documented with its escape hatch.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||
|
|
ab71913932 | Merge branch 'main' into fix/in-memory-runner-bounding | ||
|
|
77d99f6bd5 |
docs(reskinnable-demo): teach the reskin skill what the demo must prove (#6400)
## What does this PR do? Reworks the repo-local `reskin` skill so it teaches **what a skin's demo has to prove**, not just how to wire the `Skin` contract — based on David's walkthrough of the banking demo (the "beats" and the why behind each one), plus the layout facts from #6394. Docs only. No code changes. ## Why The skill documented the contract thoroughly and the demo not at all. I tested that gap before writing anything: gave a fresh agent the **old** skill and asked it to plan a new BI skin. It returned a technically sound plan — correct contract usage, server-emitted a2ui, deps arrays, OGUI sandbox, dark mode — that missed most of the beats, and substituted its own thesis (RBAC governance) for the demo's. | Beat | Old skill | Reworked skill | | --- | --- | --- | | 1. Lead with generative UI | ✅ | ✅ | | 2. Rich thread survives reload | ❌ absent | ✅ explicit step; renders keyed off `result` | | 3a. Drive the app, secret withheld | ⚠️ reframed as RBAC | ✅ confidential figure never enters the transcript | | 3b. "What's on my screen?" | ❌ **zero readables planned** | ✅ route + per-page readables, asked on two pages | | 3c. Navigate via real levers | ⚠️ plain `navigateTo` | ✅ HITL confirm → query params → highlighted controls | | 3d. Multimodal → durable artifact | ⚠️ no durability | ✅ artifact outlives thread deletion (demonstrated) | | 4. Long-term memory recall | ⚠️ one line | ✅ seeded preference + recall-first + names what it recalled | | 5. Stored-procedure replay | ❌ absent | ✅ seeded procedure, 3 visible writes, distractors | | 6. Teach a new procedure | ❌ absent | ✅ symptom-only gate, decoys, record → save → replay | | Presenter Reset | ❌ | ✅ | | Pills covering the flow | 6, partial | 8, one per beat | Same prompt, same conditions, only the skill changed. The telling detail is 3b: `useAgentContext` was already in the skill's contract table, so the agent *knew* the field existed — it just had no reason to prioritise it. Contract docs say what is available; only choreography says what is required. ## What changed **`.claude/skills/reskin/demo-beats.md`** (new) — nine beats, each framed by *what the audience must conclude*, then what the skin needs, then banking's implementation with file:line. Plus seeding rules, the presentation requirements (a pill per beat so the presenter never types, a visible affordance on every mutation, pretty markdown prose, Reset, the chat-placement framing), the vertical shortlist with BI called out as highest-stakes, the quality bar, and a "which skin to copy for what" routing table. The beats are a **strong default that an explicit instruction overrides**, expressed as a beat map with one row per beat that records deliberate skips. Omission failures respond to a slot you have to fill in, not to exhortation. **`SKILL.md`** — beat table and override rule up front; step 0 is the beat map, before code. Two new silent-failure rules found by tracing banking: - Renders must key off the tool `result`, not `status`, or they go blank on thread replay — exactly when beat 2 is being demonstrated. Only banking does this today. - Beat 3b needs a route readable plus per-page on-screen readables. All four skins register readables; only banking registers those, which is why the beat is impossible in the other three. Verification now ends with a 10-point demo walkthrough, since a green build proves the wiring and nothing about the demo. **`templates.md`** — pills-per-beat template; `seed-memories.ts` against the real `POST /api/memories` shape (long-term memory is a seeded file, not emergent — including why beat 6's procedure must never be seeded, and why a seeded procedure must run with no confirmation gate, which previously left an unresolved tool call that failed the next message); a prompt-clause table mapping clauses to beats; replay-safe render; route and per-page readables. **`CLAUDE.md`** — claimed four skins in the intro and then documented two. Now covers all four across both substrates with a beat-coverage matrix, and corrects three false claims: - `identifyUser` was attributed to banking alone; logistics and keel contribute one too. - An unsupported claim that a `Panel`'s `id` becomes its emitted `data-testid` — the cited header comment says no such thing and the installed package has no such behaviour. Replaced with what it actually documents. - "two implementations" in Reference. Also notes keel's parameterized routes (the only skin with them) and the shell-mounted inspector that replaced the glass engine. ## Notes for reviewers - Verified against source rather than assumed: the entire `Skin` contract field table in both docs was already accurate and is unchanged; so were the Commands section, all panel-size/breakpoint/radius numbers, the `.d.cts` filename the skill cites, and `defaultSkinId`. There were no glass-engine references left to remove. - Two follow-ups surfaced but deliberately not included here: `banking/components/wow/proactive-notice.tsx` is an `animate-ping` bell toast that is mounted nowhere, and logistics/keel already ship the full per-user identity plumbing with no memory prompts, tools or seed file — the cheapest beat-4 upgrades available if we want existing skins pulled up to the bar. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
b93b0951bc |
docs(reskinnable-demo): teach the reskin skill what the demo must prove
The skill documented how to wire the `Skin` contract and nothing about what the resulting demo has to demonstrate. Tested by asking a fresh agent to plan a new BI skin with only the old skill: it produced a technically sound plan that missed most of the banking demo's beats — no rich-thread step, zero `useAgentContext` readables, no stored-procedure or teach-mode arc — and substituted its own thesis (RBAC governance) for the demo's. Contract documentation says what is available; only choreography says what is required. Adds demo-beats.md: the nine beats framed by what the audience must conclude, each with banking's implementation cited, plus the presentation requirements (a pill per beat so the presenter never types, a visible affordance on every mutation, pretty prose, Reset, the chat-placement framing), the domain shortlist and the quality bar. The beats are a strong default an explicit instruction can override, expressed as a beat map with a row per beat that records deliberate skips — omission failures respond to a slot you must fill, not to exhortation. Re-running the same planning task with the reworked skill covers all nine. Also folds in what tracing banking turned up, all of it silent-failure class: renders must key off the tool `result` and not `status` or they go blank on thread replay, exactly when "reload and the chart is still there" is being shown; "what's on my screen?" needs a route readable plus per-page on-screen readables, which is why that beat is impossible in the three other skins; and long-term memory is a seeded file, not emergent, so seed-memories.ts gets a template — including why beat 6's procedure must never be seeded and why a seeded procedure must run without a confirmation gate. Corrects CLAUDE.md, which claimed four skins and then documented two. Now covers all four across both substrates with a beat-coverage matrix, fixes the `identifyUser` attribution (logistics and keel contribute one too), drops an unsupported claim that a Panel's `id` becomes its `data-testid`, notes keel's parameterized routes, and acknowledges the shell-mounted inspector that replaced the glass engine. Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
b30527ca8a |
fix(showcase-mastra): render interactive open-gen-ui on a live endpoint (#6389)
## Problem (PNI-119)
`open-gen-ui-advanced` (and its minimal sibling) did **not** render
working generative UI on a **live** endpoint, though both passed under
aimock.
Both cells reused the shared `weatherAgent` (`instructions: "You are a
helpful assistant."`, gpt-4o). On a live LLM that emitted **static HTML
with no JS wiring** — the calculator's buttons rendered but did nothing
(staging repro: `7 + 8 =` left the display on `0`; captured SSE
`generateSandboxedUi` args contained only
`initialHeight/placeholderMessages/css/html`, no
`<script>`/`jsFunctions`/`jsExpressions`).
aimock hid the divergence: its fixtures match on
`userMessage`/`hasToolResult` and replay a fully-wired UI regardless of
the agent.
## Root cause
Two factors, one dominant:
1. **No dedicated agent.** Gold `langgraph-python` uses purpose-built
`open_gen_ui_agent.py` / `open_gen_ui_advanced_agent.py` whose system
prompt mandates a single interactive `generateSandboxedUi` call (iframe
restrictions, wire host functions via `Websandbox.connection.remote.*`,
visible result element). Mastra reused a generic agent.
2. **Context is a read-channel on Mastra.** The provider delivers the
design-skill + sandbox-function descriptors via `copilotkit.addContext`
→ `RunAgentInput.context`. The `@ag-ui/mastra` bridge maps that to
`requestContext.get("ag-ui").context` (a tool read-channel) and does
**not** inject it into the LLM prompt (unlike langgraph's
`CopilotKitMiddleware`). So even the tool description alone was not
enough for gpt-4o.
## Fix (surgical, showcase-only)
- `src/mastra/agents/index.ts` — add dedicated `openGenUiAgent` /
`openGenUiAdvancedAgent` porting gold's system prompts, with **dynamic
instructions** that read `requestContext.get("ag-ui").context` and fold
the design-skill + sandbox descriptors into the prompt (surfacing the
read-channel the Mastra-idiomatic way, matching what gold's agent sees).
- `src/mastra/index.ts` — register both.
- `src/app/api/copilotkit-ogui/route.ts` — point `getLocalAgent` at the
new agents.
No published-package or bridge change.
## Verification (live)
Local `next dev` against **real OpenAI** (aimock bypassed). All three
advanced pills render interactive, host-wired UI; host round-trips
confirmed in console:
- **Calculator** → `evaluateExpression 7*8 = 56`
- **Ping the host** → `notifyHost: Ping!` (+ "Host notified
successfully." in-iframe)
- **Inline expression evaluator** → `evaluateExpression 5 + 3 * 2 = 11`
**aimock unaffected** — matcher keys
(`userMessage`/`hasToolResult`/`context`) don't touch the prompt or
agent, so D6 replay still passes. Changed files typecheck clean.
## Follow-up (out of scope)
Live surfaced a benign `@copilotkit/runtime` OGUI-middleware warning: an
empty/null `jsFunctions` yields a JSON-patch `add /jsFunctions` with no
`value` (`OPERATION_VALUE_REQUIRED`, patch dropped). Tracked separately
(needs a runtime release).
|
||
|
|
9c3c0c0148 |
feat(reskinnable-demo): improve general layout and UI (#6394)
## What does this PR do? Reworks the reskinnable demo's layout from four flush, edge-to-edge columns into a shadcn-inset-style frame: the assistant column and the skin's app each float as a rounded card, separated by a resizable gutter. **Layout** - Two panels with simple bounds — assistant `min 250px / default 600px / max 50%`, app takes the remainder. Capping the assistant as a *share* means the app needs no floor of its own and the mobile breakpoint stays a genuine 768px. - Drag to resize, swap which side the assistant docks on, or hide it entirely. Side and open state persist, shell-global, so switching skins never rearranges the workspace. - Card radius is a fixed 12px in px, deliberately *not* `--radius` — the frame is shell chrome and must read identically in every skin. Card colours stay themed, so a reskin still restyles it. **Chat** - `CopilotSidebar` → inline `CopilotChat`, which is what lets the cluster live in a panel. That deletes a whole geometry contract (`--nw-chat-width`, `--nw-rail-offset`, `data-nw-chat-open`) plus ~110 lines of CSS that existed only to keep a floating selector off a fixed panel. - The thread rail is a fixed-width element, not a nested panel — it sits beside the conversation and hides via a container query when the card is dragged narrow. **Skin selector** - Now a dropdown, so its footprint stays flat as skins are added (the pill row took three rows at four skins). Shell controls — switcher, swap sides, hide — are grouped in the card; the chat header keeps only conversation actions. **Type** - Base scale set to 15px at the root, so it reaches all four skins with no per-skin edits. ## Notes for reviewers - `react-resizable-panels` is pinned to **4.x**, whose API is renamed from the 2.x/3.x used by six sibling examples (`Group`/`orientation`/`Separator`/`useDefaultLayout`). Only 4.x accepts pixel size constraints. See the header comment in `src/components/ui/resizable.tsx` before touching it. - The chat CSS re-key landed as a deliberate bracket — duplicate onto `.nw-chat`, make the structural change, delete the originals — because its failure mode is silent: a lint-clean, test-clean build with subtly wrong chat typography. A guard test asserts first parity, then absence. - Adds `e2e/inset-layout.spec.ts` (9 tests). Weighted toward what actually broke during development — every bug here was a sizing behaviour invisible to jsdom. - The reskin skill taught two now-retired patterns (publishing `--nw-nav-inset-*`, rooting layouts at `h-screen`); both are corrected, so authoring a new skin no longer produces dead code and a broken root height. **Known limitation:** the sub-768px overlay branch is not verified. Mobile was already broken before this work and is explicitly out of scope; the e2e suite runs at 1280px. ## Checklist - [x] I have read the Contribution Guide - [x] If the PR changes or adds functionality, I have updated the relevant documentation - [x] "Allow edits by maintainers" is checked |
||
|
|
1c53a8c60b | Merge branch 'main' into feat/reskinnable-demo-ui | ||
|
|
7aab1bead2 |
docs(reskinnable-demo): document the inset frame and retire the old chrome
The reskin skill was the urgent part: it is the sanctioned path for adding a skin, and it still taught two patterns the frame retired. It told authors to publish --nw-nav-inset-left/right from a useEffect so the floating selector could dodge their nav -- both the variables and that selector are gone -- and to root the layout at h-screen overflow-hidden, which overflows the app card by the frame's padding. Following it produced a skin with dead code and a broken root height. The nav-inset step is removed with a short "Retired" note so it is not reintroduced by copying an older skin, and the root guidance now explains that the CARD is the bound. Adds a "The inset frame" section to CLAUDE.md covering what a reader needs before touching src/shell/layout: the two-panel model and its bounds, why the thread rail is a fixed element rather than a nested panel, the v4 API rename and the id-becomes-data-testid behaviour, where the shell controls live, the fixed 12px card radius, and the h-full requirement for skin layouts. Also corrects the composition chain, the shell's ownership list, and the four-skins-not-two count across CLAUDE.md, README.md and DESIGN.md, plus the skill's verification step, which told authors to look for their skin in a bottom-left floating pill. |
||
|
|
c70db12a43 |
test(reskinnable-demo): cover the inset layout end to end
Nine new tests over what only a real browser can see: panel order per docked side, resize bounds in both directions, persistence across a reload and across a skin switch, the rail collapse/reopen cycle, the hide/launcher cycle, that the selector renders options only while open, that every skin's chrome fits its card without scrolling the document, and that the chat header shows the active skin's assistant name. Weighted toward what actually broke. Every bug this layout shipped was a sizing or resize behaviour invisible to jsdom, which has no layout engine: a rail collapsed to zero width, a rail that could not be reopened, an assistant column that could be widened but never narrowed, and a header showing the framework default. All four passed a green unit suite. Each assertion was mutation-checked rather than assumed -- widening the assistant cap to 95% fails only the resize test, hardcoding the header title fails only the header test. Navigation goes through a gotoSkin helper that waits for the frame; the shell is client-rendered, so reading geometry straight after goto returns an empty panel set. Updates smoke.spec.ts for the dropdown: switching opens the menu first, and the "no other skin's chrome leaked" guards get STRICTER -- from exactly-one selector pill to zero occurrences, since a closed menu renders no other brand. Retires the CopilotSidebar launcher from the fixme'd specs. a2ui-canvas clicked copilot-chat-toggle, which no longer exists -- inert only because the spec never runs, so it would have failed the moment anyone re-enabled it. memory-learning keeps its guarded open-chat block deliberately: the count check makes it a no-op today, and that spec needs the docker memory stack so it cannot be run here to verify a behavioural edit. |
||
|
|
0c8d8db275 |
feat(reskinnable-demo): mount the frame, free the skins, retire the floating selector
Composes ShellFrame in the per-skin layout, passing the skin's Layout as its app slot and the shared ChatPanel as its chat slot, replacing the sibling Layout + ChatPanel + FloatingSelector trio. Each skin's chrome drops its viewport height for h-full: it now fills the app card, which the frame has already inset by its own padding, so a viewport-height root overflowed the card by exactly that much. Logistics keeps overflow-hidden -- its nav stays pinned only while the container is bounded, which is now the card. Banking keeps its second, unrelated canvas-clearing effect and therefore its useEffect import. The four --nw-nav-inset-* publishing effects go with the floating selector they fed. Each skin published the width of its own edge-nav so a floating pill could compute a safe band and dodge it; the selector now occupies a slot in the assistant column and overlaps nothing, so the whole mechanism is unnecessary and its component is deleted. This is one commit because the halves are not independently correct: without the skin edits the cards overflow, and deleting the selector before its import is removed would not build. |
||
|
|
58efc192e4 |
feat(reskinnable-demo): render the chat inline inside a resizable card
Replaces CopilotSidebar with an inline CopilotChat so the cluster can live in a panel. CopilotSidebar was a fixed <aside> that pushed document.body's margin and faked two columns by being handed the width of both, insetting its own contents past the rail while the rail painted into the freed strip as a separately-fixed sibling. That deletes an entire geometry contract: --nw-chat-width, --nw-rail-offset and data-nw-chat-open existed only to describe a fixed panel's footprint to the rest of the page. Also gone: the force-open-on-mount ref dance, the header slot cast (CopilotChat has no header slot -- that is a modal concern, so the header is now an ordinary sibling), and the rail's own fixed positioning and translate animation. The thread rail is a FIXED-WIDTH element, not a panel. v4's collapse API fought that three ways: a collapsed 0 written to storage was restored forever, expand() restores the "most recent size" which after collapsing to 0 is 0, and resize() is ignored while collapsed. Conditionally rendering it makes isInboxOpen the single source of truth with no imperative sync. The header reads the SKIN for its title, not the chat configuration. As CopilotSidebar's header slot it rendered inside the chat's own provider so our labels reached it; as a sibling it reads the wrapper's, whose default "CopilotKit Chat" is non-null and won the ?? chain -- every skin's header showed that instead of its assistant name. The header is skin chrome now, and holds only conversation actions. |
||
|
|
06f973823e |
feat(reskinnable-demo): add the inset shell frame
src/shell/layout/ owns the app's outer geometry: a padded region holding the assistant column (selector card above chat card) and the skin's app card, separated by a resizable gutter. The model is deliberately just "one bounded panel, one that takes the remainder" -- assistant min 250px / default 600px / max 50%, app gets what is left. An earlier version nested the thread rail as a resizable panel INSIDE the assistant column, which made its floor a compound of rail + conversation and forced a derived breakpoint, a switching collapsed floor and an app floor to compensate. Capping the assistant as a SHARE rather than a pixel count is what removes the need for an app floor and lets the mobile breakpoint stay a genuine 768px instead of being derived from panel arithmetic. - selector-card: the skin switcher as a dropdown, so its footprint stays flat as skins are added, plus the shell controls -- swap sides and hide. All three are shell concerns, which is why they are here and not in the chat header. useSkinThemeReconcile stays on the card root: it reads the computed --nw-dark-capable from inside the skin's theme root, and losing it would let a light-only skin render dark chat chrome. - layout-preferences: side and open state, shell-global so switching skins never rearranges the workspace. Read through useSyncExternalStore because this repo treats react-hooks/set-state-in-effect as an error, with write-through persistence so only a deliberate choice is stored. The hook returns inert defaults outside its provider rather than throwing. - use-is-desktop: matchMedia through the same external-store pattern. - panel-sizes: the three numbers, in one place. |
||
|
|
a36ab28019 |
refactor(reskinnable-demo): re-scope the chat CSS and set the base type scale
Three changes to the shell stylesheet. Re-scopes chat typography from the SDK's [data-copilot-sidebar] attribute to a .nw-chat wrapper. That attribute came from CopilotSidebar, which the inset frame replaces with an inline chat, so ~23 rule blocks styling assistant markdown would have silently stopped matching -- a lint-clean, test-clean build with subtly wrong chat text. The migration ran as a bracket: duplicate onto .nw-chat, land the structural change, then delete the originals, with a guard test asserting first the parity and then the absence. Typography is provably unchanged -- a probe mounting markdown-shaped content with the library's prose class reported 140 identical computed values across 14 selectors before and after. Adds the frame's own rules: .nw-panel-card (a FIXED 12px radius, deliberately not reading --radius, because the frame is shell chrome and must read identically in every skin) and the chat cluster's fixed-width rail with the container query that hides it when the card is dragged narrow. Sets the root font size to 15px. The root, not body: every rem in the shell and all four skins resolves against it, so this is the one lever that rescales type everywhere without per-skin edits. Spacing scales with it by design; anything that must not move is written in px. The chat prose moves 0.9375rem -> 1rem, since that value existed to hit 15px under a 16px root and would otherwise sit below the new default. Deletes the ~120-line floating-selector dock and the unused .brand-text-gradient. |
||
|
|
6c7e7b7df4 |
feat(reskinnable-demo): add the react-resizable-panels v4 wrapper
Pins ^4.12.2 and wraps its Group/Panel/Separator API behind two styled handles: an 8px gutter for the gap between cards and a 1px hairline. 4.x rather than the 2.x/3.x used by six sibling examples because only 4.x accepts PIXEL size constraints, and the layout's bounds are pixels. Three v4 behaviours the consumers depend on, each with a test: - The API is RENAMED from 2.x/3.x: PanelGroup->Group, direction->orientation, PanelResizeHandle->Separator, autoSaveId->useDefaultLayout. Most material online, including shadcn's Resizable block, targets the old names and will not compile. - data-testid is DERIVED from a Panel's id and overwrites any passed in, so a panel's id is its query handle. Getting this wrong makes every [data-testid$='-panel'] query silently match nothing. - Separator refuses flex-grow/flex-shrink overrides, so shrink-0 would be dead. Adds a jsdom ResizeObserver stub, without which every test rendering a Group throws. Note for anyone adding a dependency here: declare it in package.json and run 'pnpm install --lockfile-only' from the REPO ROOT. This example pins pnpm 10.10.0 while the root pins 10.33.4, so 'pnpm add' from this directory makes corepack switch versions and the older pnpm re-resolves the entire workspace lockfile. |
||
|
|
c2cb1c9e60 |
fix(showcase-mastra): render interactive open-gen-ui on a live endpoint
Both open-gen-ui cells reused the shared weatherAgent
("You are a helpful assistant.", gpt-4o). On a live LLM that produced
static HTML with no JS wiring - buttons rendered but were dead. aimock
hid it: fixtures match on userMessage/hasToolResult and replay a
fully-wired UI regardless of the agent.
Gold langgraph-python uses dedicated agents whose system prompt mandates
a single interactive generateSandboxedUi call and reads the design-skill
+ sandbox-function descriptors from copilotkit context. On Mastra,
RunAgentInput.context is a read-channel (requestContext.get("ag-ui")),
not injected into the prompt, so that guidance never reached the model.
Add dedicated openGenUiAgent / openGenUiAdvancedAgent porting gold's
prompts, with dynamic instructions that fold the ag-ui context into the
system prompt, and point the ogui route at them.
Verified live (real OpenAI, aimock bypassed) - all three advanced pills
render interactive, host-wired UI with confirmed round-trips:
evaluateExpression 7*8=56, notifyHost, evaluateExpression 5+3*2=11.
aimock matcher keys are untouched, so replay is unaffected.
|
||
|
|
2f24038c0d |
chore(skills): sync in-memory runner reference skills and mirror
Updates the runtime agent-runner skill references to match the bounded in-memory runner: correct the InMemoryAgentRunner store as a process-global singleton, document its bounds and onConcurrentRun concurrency handling, note that dedup weakens past the run cap, fix rotted in-memory.ts citations onto stable symbols, and correct the multi-instance SqliteAgentRunner scaling guidance. The generated skills/ mirror is regenerated in lockstep so source and mirror stay in sync. Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
c91376452a |
docs(runtime): document in-memory runner bounds, concurrency, and durability
Documents the now-bounded in-memory runner for users: the maxThreads / maxRunsPerThread / maxBytes limits and their defaults, the precise eviction model (LRU threads, per-thread run-cap, cross-thread byte ceiling enforced at run completion), and the onConcurrentRun throw/supersede option. Clarifies that the store is a process-global singleton shared by every runner, that dedup weakens past the run cap, and points at the first-party SqliteAgentRunner for durable or multi-instance deployments. Adds a troubleshooting entry for the in-memory eviction warning. Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
8f56cc9457 |
test(runtime): cover bounded store, eviction, and run teardown isolation
Adds a dedicated bounded-thread-store suite and extends the in-memory runner suite to lock in the behaviors introduced with the bounded, teardown-isolated runner: - LRU thread eviction, per-thread run-cap FIFO trimming, and byte-ceiling eviction (including that a live/running or stop-requested thread is never evicted, and that a just-appended thread pushes OTHER threads out rather than self-evicting). - InMemoryLimits validation/normalization: invalid bounds clamp to defaults instead of crashing enforceRunCap, and the 0/Infinity disable sentinels are preserved. - Thread-level createdAt and message-snapshot decoupling survive run-cap eviction and interleaved empty-snapshot runs. - stop() guards and the supersede path: an aborted run finalizes as a clean RUN_FINISHED against its own captured intent, a superseded run cannot clobber its replacement's state or history, and an immediate abort-throw that emitted nothing creates no phantom historic run. Also restores the shared store's default limits after tests that reconfigure the process-global store so suites stay isolated, and updates a handle-run comment for the GLOBAL_STORE -> shared store rename. Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
e93119ae20 |
fix(runtime): isolate a superseded or stopped in-memory run's teardown
A run's async teardown used to read the shared, mutable store.stopRequested to decide whether to finalize as a clean RUN_FINISHED or a synthetic RUN_ERROR. Under supersede (or a stop() immediately followed by a new run()), a later run resets that shared flag, so the earlier run's fire-and-forget finalization could be mislabelled — an intentionally stopped run finalized as an error, or vice versa — and could push history under, or clobber the state of, the newer run that now owns the thread. Fix by capturing a per-run RunFinalizeControl when the run starts. stop() and a superseding run() flip THAT run's captured control (not just the store flag), and the run's teardown reads its own captured intent, so a later run resetting store state can never change how an earlier run finalizes. The teardown itself is unified into a single finalizeRun helper shared by the success and error paths (they were near-identical and must stay symmetric) and made ownership-aware: - It only pushes history / resets shared store state when this run still owns the thread (store.currentRunId still equals this run's id), so a superseded run cannot corrupt the successor's history or state. - The error path additionally requires at least one real (pre-finalize) event, reviving a guard that had gone dead: an immediate throw that emitted nothing must not create a phantom historic run holding only the synthetic terminal. - On completion it releases the run's infinite ReplaySubject buffer via an identity guard (store.subject === nextSubject), reclaiming the duplicate buffer on the owning path while leaving a live successor's subject untouched. The concurrency branch now also triggers on store.stopRequested, not just isRunning: stop() flips isRunning off the instant it aborts but the run keeps finalizing, and a run() slipping through that window went entirely unhandled. The previous-subject bridge is removed: forwarding a dying superseded run's subject would replay its RUN_STARTED and push its terminal into the live run's stream, an invalid AG-UI sequence — a superseded run must stay isolated to its own subscribers. Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
05fbd70938 |
fix(runtime): delegate InMemoryAgentRunner storage to the bounded store
Route every storage access in the runner through the shared ɵBoundedThreadStore instead of the old unbounded GLOBAL_STORE Map: run() acquires threads via getOrCreate (which applies LRU eviction), and connect/isRunning/stop/ listThreads/getThreadMessages/getThreadEvents/clearThreads read through the store's touch-aware accessors so reads keep LRU order honest. The constructor now accepts InMemoryLimits inline alongside onConcurrentRun. Note the scope difference, called out in the JSDoc: onConcurrentRun is per-runner, but the limits reconfigure the PROCESS-GLOBAL store shared by every runner. A partial limits update coalesces each unspecified field against the store's current effective bounds (not the hardcoded defaults), so tuning one bound never silently resets its siblings; a genuine clobber of an already-customized store warns once. getThreadMessages now returns the thread-level snapshot (a shallow array-level copy) rather than the last run's snapshot, so run-cap eviction and interleaved empty-snapshot runs can never lose it. getThreadState is hardened to reject arrays (which pass `typeof === "object"`) and to return a defensive shallow copy so callers cannot mutate stored snapshot state. Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
4bd4fff6a3 |
fix(runtime): add bounded in-memory thread store with limits validation
The in-memory runner previously kept every thread and run forever in an unbounded process-global Map, so a long-lived process leaked memory without limit. Introduce ɵBoundedThreadStore as the single backing store, enforcing three independent bounds resolved from InMemoryLimits (defaults in ɵINMEMORY_DEFAULTS): - maxThreads: LRU eviction of whole threads. - maxRunsPerThread: FIFO run-cap per thread. - maxBytes: approximate cross-thread byte ceiling (via ɵestimateBytes), enforced at run completion by evicting other LRU non-running threads. Limit values are validated and normalized once (ɵnormalizeLimits / ɵisValidLimit): only a non-negative integer or +Infinity is well-formed. Invalid values (negatives, -Infinity, NaN, fractional caps) would otherwise turn the `count > limit` enforcement guards into infinite loops or a shift() of undefined; they are instead clamped to the documented default with a single warning. Clamp-and-warn rather than throw matches this file's best-effort posture (ɵestimateBytes swallows serialization failures), because constructing a non-durable convenience runner must never abort — or later surface an unhandled rejection — on a typo'd bound. Thread creation time and the latest non-empty message snapshot are held at the THREAD level (InMemoryEventStore.createdAt / messagesSnapshot), decoupled from historicRuns so run-cap FIFO eviction can neither drift the reported creation time forward nor drop the message history. Eviction — whole-thread LRU and per-thread run-cap trimming alike — is logged once per store (warn-once latch) so bounded history loss is visible rather than silent. Also defines the per-run RunFinalizeControl shape and the store's activeFinalize holder that the run-teardown isolation builds on. Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
25817605cc |
fix(reskinnable-demo): put real data behind the banking report, and fix the six defects it was hiding (#6378)
Fixes the six defects a 12-agent review found in the banking skin, by
fixing the thing that caused most of them: the skin had **three
disagreeing answers to "what did we spend"**.
| Source | Total | Drove |
|---|---|---|
| `charges-data.ts` fixture (45 rows, Apr–Jun) | $632,806 | the Charges
page |
| `seed.json` ledger (4 rows, **Apr–May only**) | $30,089 | the report's
**charts** |
| static `policies[].spent` | $137,000 | the report's **KPI** |
Every one of those numbers appeared in the product, and they were never
reconciled.
## The headline defect
`SpendingTrendChart` substituted a hard-coded `[3200, 4100, 3600, 5200,
4800, 6400]` / Jan–Jun series whenever fewer than three distinct months
were present. Intended as an empty state — but the ledger spanned
exactly **two** months, so the fallback was the **default path**. The
report's "Spend over time" showed six invented figures, roughly 20×
smaller than the total printed directly above them, under a card whose
own docstring reads:
> Every number is computed here from the live ledger … so a report can
never quote a figure the app disagrees with.
Reproduced in the running app before fixing (`POST
/api/banking/v1/reports`, no agent needed), and there's a nasty
interaction worth knowing: attaching an invoice dates a synthetic
transaction *today*, supplying a third month, so **attaching an invoice
masked the bug**. A test written casually would sit in the masked state
and pass.
## The fix: one ledger
The 45 charges now live in `seed.json` as real transactions across
**Apr/May/Jun**, and the Charges page reads them over REST like every
other surface.
**Team and policy are now different axes.** A charge belongs to one of
seven org teams; a policy is one of three budget envelopes (Technology /
Go-to-Market / G&A) and several teams share one. They were a single
`ExpenseRole` enum — which is exactly why covering every team meant
choosing between a seven-slice donut and discarding real charges.
`ExpenseRole` still types a member's own team; `PolicyType` types the
envelopes, joined by `policyForTeam`.
**`policies[].spent` is derived from approved charges on every read.**
It could no longer disagree with the charts — and it also now *moves*:
approving a charge previously left `spent` untouched, so the budget
never reflected the approval and the over-limit gate kept comparing
against a stale figure. Verified live: approving a \$960 charge moved
`spent` by exactly \$960.
**Over-limit is derived-only.** A charge no longer *stores*
`over-limit`; the Charges badge resolves through `withOverLimit`, the
same rule the report uses.
## The six review findings
| | Defect | Fix |
|---|---|---|
| a1 | report charted fabricated spend | charts real months; explicit
empty state at zero |
| a2 | donut its own docs said would collapse | real \$533k base — the
\$900k invoice that hit **89%** now reaches **73%**; 89% would need
~\$2.8M |
| a3 | `report` matched inside "quarterly report" | anchored both sides,
whole words |
| a4 | `as Transaction` laundered a nullable `policyId` | cast removed;
compiler checks it |
| a5 | comment claimed additions "have no policyId" | corrected — three
lines from the code that sets it |
| a6 | `?sort=banana` lit the "active" tint | params validated; unknown
reads as unset |
All six existed **identically in `examples/showcases/banking/`** — they
came from the upstream PRs this skin replayed, not from the port. Scoped
to the skin per review; banking still carries them.
## The scripted demo is unchanged by construction
- the four demo-load-bearing transactions survive byte-identical
- over-limit is still **exactly 3 charges / \$30,000**
- AWS \$15,000 still derives over-limit for the teach-mode pill
- Delta Airlines is still the only Delta charge (the fixture's
near-duplicate "Delta Air Lines" became United Airlines)
- all four status badges still appear (`Amazon Business` is kept
pending, under its policy's headroom, so a plain **Pending** chip
survives)
The donut goes 44/40/16 → **48/27/24** and is relabelled "Spend by
policy", which is what it reads.
## Verification
```
nx build react-core,a2ui-renderer,core,runtime,shared exit 0
tsc --noEmit 0 errors
vitest 239 passed (was 215)
eslint 0 problems
```
Plus live checks against a running server: 49 transactions over 3
months, derived `spent` tracking an approval, over-limit holding at
3/\$30,000.
The 24 new tests are confirmed **red** against the old code —
`parseSort("banana")` returned `"banana"`, `parseTop("-5")` returned
`-5`, the seed spanned two months, no row carried a team, and `spent`
was stored.
## Not in this PR
A "tool replay-guard sweep" (4 items, `navigateToPageAndPerform` and
three approval tools missing the resolved-state guard `showCharges` has)
and ~13 subject-neutral items are captured as follow-ups.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||
|
|
8829633e29 | Merge branch 'main' into fix/banking-skin-real-data | ||
|
|
addbe2888d |
test(reskinnable-demo): lock in the single-ledger invariants
Two new suites, each written so it would have failed against the previous code
rather than merely describing the new behaviour:
- derived-spend: policies[].spent equals the sum of approved charges, excludes
pending/flagged (so the over-limit check cannot double-count the charge it is
gating), and agrees between policies() and findPolicy(). Plus the properties
the demo depends on — at least three distinct months (the condition whose
absence made the trend chart fabricate), AWS $15,000 still deriving
over-limit for the teach-mode pill, exactly one Delta charge, and a team and
category on every ledger row.
- charges-data: parseSort and parseTop reject the values that previously slipped
through as valid, and toChargeRow's over-limit projection only applies to
pending charges.
Confirmed red against the old code: parseSort("banana") returned "banana",
parseTop("-5") returned -5, the seed spanned two months, no row carried a team,
and spent was a stored field.
The four existing fixtures move from ExpenseRole to PolicyType for policy
`type`, following the team/policy split.
|
||
|
|
d7e7c8eaf4 |
fix(reskinnable-demo): stop the alert marker firing on ordinary notes
The regex that decides whether a transaction note gets a red-alert prefix was anchored on the left only, so `report` matched inside "reporter" and "quarterly report" and `disput` inside "disputation". A note reading "attached to the quarterly report" was served a fraud marker. Anchored on both sides and switched from stems to whole words, verified against the four phrasings that previously false-positived. |
||
|
|
05390dc625 |
fix(reskinnable-demo): stop the banking report showing invented figures
Four defects in the report's charts, all reported by review and all reproduced in the running app before fixing. - SpendingTrendChart substituted a hard-coded [3200, 4100, 3600, 5200, 4800, 6400] Jan-Jun series whenever fewer than three months were present. Intended as an empty state, it was the DEFAULT path: the seeded ledger spanned two months, so the report's "Spend over time" always showed six invented numbers — roughly 20x smaller than the total printed directly above them — under a card whose own contract says every number comes from the live ledger. It now charts whatever months exist, with a real empty state at zero. - SpendBreakdownChart's docstring said the report must use SpendByTeamBars instead, "because an attached invoice can push one team to ~96% and a donut cannot survive that", while the report rendered the donut anyway. The warning was real but its cause was the thin ledger, not the chart: against the old $137,000 base a $900,000 invoice took one slice to 89%. Against the real ~$533,000 base the same invoice reaches 73%, and 89% would need ~$2.8M. Robust because the data is real, not because a floor was added to the arc. - augmentForReport built its synthetic transactions behind an `as Transaction` cast that was hiding a real hole: `policyId` came from an `?.id` lookup, so it could be undefined where the field is a required string. The cast is gone and the compiler checks it. Additions also now resolve their model-authored team to a policy envelope through `policyForTeam` rather than comparing a team name to a policy name, with one "Unattributed" segment for unmappable names. - A comment inside TopChargesChart claimed document-sourced charges "have no policyId" — three lines above the code that gives them one. The donut column is relabelled "Spend by policy", which is what it reads. |
||
|
|
274d46ffcb |
feat(reskinnable-demo): put one real ledger behind the banking skin
The skin held three disagreeing answers to "what did we spend": a 45-row Charges fixture ($632,806), a 4-row seeded ledger ($30,089 across two months), and static policy totals ($137,000). Each surface read a different one, so they drifted silently — and because the ledger spanned only two months, the report's trend chart fell back to a hard-coded series and showed invented figures under a card that promises live numbers. Now there is one ledger. The 45 charges live in seed.json as real transactions across Apr/May/Jun, and the Charges page reads them over REST like every other surface. - Splits team from policy. A charge belongs to one of seven org teams; a policy is one of three budget envelopes (Technology / Go-to-Market / G&A) and several teams share one. These were a single `ExpenseRole` enum, which is why the two axes read as one thing and why covering every team meant either a seven-slice donut or discarding real charges. `ExpenseRole` still types a member's own team; `PolicyType` types the envelopes, joined by `policyForTeam`. - Derives `policies[].spent` from approved charges on every read, so it can no longer disagree with the charts. It also now MOVES: approving a charge previously left `spent` untouched, so the budget never reflected the approval and the over-limit gate kept comparing against a stale figure. - Makes over-limit derived-only. A charge no longer stores "over-limit"; the Charges table resolves the badge through `withOverLimit`, the same rule the report uses, so the two cannot disagree. - Validates the `?sort=` and `?top=` params. `?sort=banana` used to be cast straight to a SortKey and lit the control's "active" tint while the table silently sorted by the default; `?top=-5` reached `slice(0, -5)` and dropped the LAST five rows, inverting top-N. The scripted demo is unchanged by construction: the four demo-load-bearing transactions survive byte-identical, over-limit is still exactly three charges totalling $30,000, AWS $15,000 still derives over-limit for the teach-mode pill, and Delta Airlines is still the only Delta charge (the fixture's near -duplicate "Delta Air Lines" became United Airlines). |
||
|
|
c236e33978 |
chore: release monorepo v1.66.2 (#6376)
## Release monorepo v1.66.2 **Scope:** `monorepo` | **Bump:** `patch` --- ### How this release process works 1. **This PR was created automatically** by the "release / create-pr" workflow. It bumped the `monorepo` packages to `1.66.2` 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 `monorepo` packages to npm at version `1.66.2` - Creates git tag `monorepo/v1.66.2` - 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.v1.66.2 |
||
|
|
53b772552f | chore: release monorepo v1.66.2 | ||
|
|
cc9b74bdb1 |
chore: release channels v0.7.3 (#6375)
## Release channels v0.7.3 **Scope:** `channels` | **Bump:** `patch` --- ### How this release process works 1. **This PR was created automatically** by the "release / create-pr" workflow. It bumped the `channels` packages to `0.7.3` 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.7.3` - Creates git tag `channels/v0.7.3` - 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.7.3 |
||
|
|
b95a43e254 | chore: release channels v0.7.3 | ||
|
|
c1adb14c48 |
chore(showcase): pin opentelemetry-api<1.44 for pydantic-ai v1 (#6374)
`showcase/integrations/pydantic-ai` cannot be installed from scratch today. A clean `pip install -r requirements.txt` produces an agent that dies on import: ``` ModuleNotFoundError: No module named 'opentelemetry._events' ``` pydantic-ai 1.0.18 imports `opentelemetry._events`. That module was removed in **opentelemetry-api 1.44.0** (last present in 1.43.0). pydantic-ai declares an unbounded `opentelemetry-api>=1.28.0`, so a fresh resolve picks 1.44.0 and the import fails. ## Why nothing was red `Dockerfile:22-24` copies **only** `requirements.txt` before `pip install`, so that layer's cache key depends on nothing else. The agent image has been reusing a pip layer baked before 1.44.0 shipped (2026-07-16) — including a successful build earlier today. The failure was masked by Docker layer caching, not absent. Any change to `requirements.txt`, a cache eviction, or a `--no-cache` build surfaces it. Note that this PR busts that cache by definition, so CI's image build is a genuine fresh-resolve test of the fix rather than a cached pass. ## Verification Run with `pip` in a clean 3.12 venv, matching how the Dockerfile installs: ``` RED (main) opentelemetry-api 1.44.0 -> import pydantic_ai raises ModuleNotFoundError GREEN (this branch) opentelemetry-api 1.43.0 -> import pydantic_ai + pydantic_ai.ag_ui OK ``` Only the ceiling is added; `opentelemetry-sdk` follows to 1.43.0 on its own, so a second pin isn't needed. No resolution conflict with `logfire>=4.10.0`. Docker and the `--d6` probe stack aren't available in my environment, so the mandatory value-test per `showcase/AGENTS.md` rule 4 has not been run locally — CI's image build and the dojo cells are the real gate here. ## Scope One line plus a comment recording why it exists and when to remove it. The comment matters: an undocumented pin is what caused the sibling `starlette==0.45.3` rot in #6363, where a correct-when-written pin outlived its reason and silently capped pydantic-ai a full major. The ceiling should come off when this package moves to pydantic-ai v2, which requires `opentelemetry-api>=1.28.0` without needing `_events`. |
||
|
|
224dfb0d8c |
fix(channels): reconnect after clean gateway close (#6371)
## Summary - replace Phoenix's retained closed transport when a live managed session receives an unexpected WebSocket close with code 1000 - reconnect and rejoin through the existing Phoenix channel lifecycle - preserve intentional session disconnects as terminal - cover the clean-close recovery path with a regression test ## Root cause Phoenix 1.8.4 does not schedule its reconnect timer for close code 1000. CopilotKit still transitioned the managed session to `reconnecting`, so the runtime reported that Phoenix was retrying indefinitely even though Phoenix never created another transport. ## Impact A brief gateway interruption that cleanly closes an established socket can now recover once the gateway is healthy, instead of leaving the runtime stuck and repeatedly logging the managed-session-down warning. ## Verification - `pnpm nx run @copilotkit/channels-intelligence:test` — 188 tests passed - `pnpm nx run @copilotkit/channels-intelligence:check-types` — passed - `pnpm nx format:check --files=packages/channels-intelligence/src/realtime-gateway.ts,packages/channels-intelligence/src/realtime-gateway.test.ts` — passed - repository pre-commit test, package validation, and lint gates — passed |
||
|
|
7e30957976 | fix(channels): reconnect after clean gateway close | ||
|
|
80fa407978 |
chore(showcase): pin opentelemetry-api<1.44 for pydantic-ai v1
pydantic-ai 1.0.18 imports opentelemetry._events, removed in opentelemetry-api 1.44.0. Its own floor is an unbounded opentelemetry-api>=1.28.0, so a fresh resolve of this package's requirements picks 1.44.0 and import pydantic_ai fails with ModuleNotFoundError. The agent image kept building because the Dockerfile copies only requirements.txt before pip install, so that layer stayed cached from before 1.44.0 shipped (2026-07-16). Any requirements change or cache eviction would have surfaced it. Verified with pip in a clean 3.12 venv, as the Dockerfile installs: before: opentelemetry-api 1.44.0, import pydantic_ai raises after: opentelemetry-api 1.43.0, import pydantic_ai OK |
||
|
|
3eaeeb19ce |
fix(channels): preserve Slack provider diagnostics (#6373)
## Summary - serialize Slack Carousel cards through elements and validate Card and Carousel payloads before provider calls - keep bounded provider diagnostics from ChannelDeliveryError through RUN_ERROR and ChannelCanonicalRunError - log low-cardinality error fields without validation messages or provider bodies - move best-effort Slack status cleanup failures to debug logs ## Why Invalid nested Slack Card payloads failed at the provider with no useful path at the application boundary. This change catches known Card and Carousel shape errors locally and preserves safe JSON pointers when Slack rejects a payload. ## Compatibility The new error details are optional. Existing consumers keep their current behavior, so no protocol version bump is needed. ## Validation - NX_DAEMON=false pnpm nx run-many -t test,check-types,build --projects=@copilotkit/channels-core,@copilotkit/channels-intelligence,@copilotkit/channels-slack,@copilotkit/runtime --skip-nx-cache - pnpm lint - pnpm check:channel-native-catalogs - targeted oxfmt and oxlint checks - git diff --check - pre-commit test, publint, and attw checks for affected packages Companion Intelligence PR: https://github.com/CopilotKit/Intelligence/pull/759 |
||
|
|
cdd605c672 |
docs(pydantic-ai): port integration docs and demos to Pydantic AI v2 (#6367)
This pull request was posted by Claude Code using claude-opus-5 on behalf of David. David has not reviewed this diff line by line. Closes https://github.com/CopilotKit/CopilotKit/issues/6363 `Agent.to_ag_ui()`, `AGUIApp` and the whole `pydantic_ai.ag_ui` module were removed in Pydantic AI v2. The docs installed pydantic-ai unpinned, so following the quickstart today gets 2.22.0 and fails twice: first at resolution (`starlette==0.45.3` conflicts with the `>=0.46.2` the `ag-ui` extra requires), then at `AttributeError`. ## What changed **8 doc pages** under `showcase/shell-docs/src/content/docs/integrations/pydantic-ai/` (`quickstart.mdx`, `quickstart/pydantic-ai.mdx`, `human-in-the-loop.mdx`, `human-in-the-loop/agent.mdx`, `generative-ui/tool-rendering.mdx`, and the three `shared-state/` pages): - the agent is served from a Starlette route via `AGUIAdapter.dispatch_request(request, agent=agent)` - `StateDeps` imports move from `pydantic_ai.ag_ui` to `pydantic_ai.ui` - install commands exact-pin `pydantic-ai-slim[ag-ui,openai]==2.22.0` and `ag-ui-protocol==0.1.19`, matching the starter fleet, plus `starlette>=0.46.2` since the snippets import Starlette directly **Per-request deps.** Every stateful snippet builds `StateDeps` inside the request handler: ```python async def run_agent(request: Request) -> Response: return await AGUIAdapter.dispatch_request( request, agent=agent, deps=StateDeps(AgentState()) ) ``` `dispatch_request` validates the client's state into `deps.state` (`pydantic_ai/ui/_adapter.py`, `run_stream_native`), so a module-level instance shared across requests lets concurrent runs clobber each other. The old `to_ag_ui(deps=...)` snippets all did this. **`examples/canvas/pydantic-ai`** — `requirements.txt` pinned, `agent/agent.py` ported, README corrected. **`examples/showcases/pydantic-ai-todos`** — `pyproject.toml` pinned and `uv.lock` regenerated (it was still resolving 1.0.10), `agent/main.py` ported, `src/agent.py` and `src/tools.py` imports moved, README and `src/app/api/copilotkit/route.ts` comments corrected. **`skills/copilotkit-integrations`** — beyond the issue's file list: `SKILL.md`, `sources.md` and `references/integrations/pydantic-ai.md` also taught `to_ag_ui()`. Same rot, same fix. ## Verified by execution The reason these docs rotted is that nothing runs them, so everything below was actually run, not read. - Both install commands were run verbatim in throwaway environments. `uv add 'pydantic-ai-slim[ag-ui,openai]==2.22.0' 'ag-ui-protocol==0.1.19' 'starlette>=0.46.2' uvicorn` and the `pip install` equivalent both resolve, landing pydantic-ai-slim 2.22.0, ag-ui-protocol 0.1.19, starlette 1.3.1. - Every ```python fence on the 8 doc pages was extracted, `exec`'d, and driven with a real `RunAgentInput` POST through `starlette.testclient.TestClient` with the model overridden to `TestModel`. All 8 return 200 `text/event-stream` with a `RUN_STARTED` ... `RUN_FINISHED` sequence and no `RUN_ERROR`. - The canvas agent was installed from its `requirements.txt` and driven the same way: 200, SSE, `RUN_STARTED` ... `TOOL_CALL_*` ... `STATE_SNAPSHOT` ... `RUN_FINISHED`. - The todos agent was installed with `uv sync --frozen` from the regenerated lock and driven the same way. Two sequential requests, one seeding a todo and one sending empty state, each saw only their own state, confirming the per-request deps actually isolate. Not executed: the Next.js frontends and the docs site build (no `node_modules` in this checkout). The TypeScript edits are comment-only. ## Deliberately out of scope `showcase/integrations/pydantic-ai` is left on its v1 fleet pin. It is 418 files, 19 mounts and 190 e2e specs, and CopilotKit said they will take it as https://github.com/CopilotKit/CopilotKit/issues/6364. The dojo and the docs therefore diverge until that lands. The CI guard from the issue's last acceptance criterion is not built here. A proposal for it is posted on https://github.com/CopilotKit/CopilotKit/issues/6363 for the team to own. Two pre-existing malformed code fences were fixed in passing, because leaving them meant the ported snippets still would not run: `quickstart/pydantic-ai.mdx` and `shared-state/predictive-state-updates.mdx` each had TypeScript embedded inside an unterminated ```python fence. The TypeScript now sits in its own fence. Overlaps with https://github.com/CopilotKit/CopilotKit/pull/6355, which ports `examples/integrations/pydantic-ai`. No file overlap. |
||
|
|
80370b5ecb | fix(channels): preserve provider diagnostics | ||
|
|
ef531035e6 | fix(channels-slack): validate card payloads locally | ||
|
|
79080816cd | fix(channels-slack): serialize carousel cards as elements | ||
|
|
aae4c0bd5d |
chore: release channels v0.7.2 (#6372)
## Release channels v0.7.2 **Scope:** `channels` | **Bump:** `patch` --- ### How this release process works 1. **This PR was created automatically** by the "release / create-pr" workflow. It bumped the `channels` packages to `0.7.2` 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.7.2` - Creates git tag `channels/v0.7.2` - 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.7.2 |
||
|
|
1f5c70da2e | chore: release channels v0.7.2 | ||
|
|
f1b42e2e0a |
feat(skills): make setup-slack-channel drive the browser by default (refs OSS-705) (#6370)
## Summary Retesting the Slack setup end to end found the skill **too cautious to be useful**. It stopped at almost every step, where the earlier version stopped only for passwords and got through — and the run was rescued by the developer telling the agent outright to just control their browser, which cut human involvement down to typing passwords. A run that pauses at every control is slower than the manual path it replaced. Three changes, all pulling the same direction. ## 1. Driving is now the default, not implicit The skill said *"most of this workflow happens in a browser"* — descriptive, and it never told the agent to **drive** that browser. It now does, explicitly, and checks its own capability before Phase 0 rather than assuming either way. ## 2. When there is no browser, ask for one — per harness Generic advice is useless here, because enabling browser control differs by harness. The agent now works out which one it is in and names the single applicable route: Claude Code and Codex each ship their own support and enable it differently, most other harnesses take a general browser-use MCP server such as Playwright MCP. **If it isn't sure, it looks it up rather than guessing.** It also names the payoff — driving turns this into typing three secrets, the fallback is roughly fifteen manual browser steps — because that is what turns a shrug into a yes. The step-by-step walkthrough survives only for an explicit decline. ## 3. Consent is batched into one authorization Phase 0 now takes **one** yes naming the whole sequence: the Slack app from the wizard manifest, its install into a named workspace, the Channel, the adapter attach, and the project key. Phase 3 and `references/intelligence-channel.md` previously required *"state what you are about to change, get an explicit yes"* for **every** dashboard goal — and the reference said it twice, back to back. That is the concrete source of the stop-at-every-step behaviour. Reading the page before acting stays required; it is no longer a reason to check in. ## What did not change The secret boundary. The developer still types the bot token, signing secret, and API key themselves, and those remain the only mandatory stops alongside anything the authorization did not cover. Batching consent must not batch away a password — that is called out in the text. `version` bumped 1.0.0 → 1.1.0. ## Validation - `tsx scripts/sync-plugin-skills.ts --check` → **plugin skill mirror in sync** - `scripts/__tests__/sync-plugin-skills.test.ts` → 10 tests passed - `prettier --check` clean on both changed files ## Companion The same shift landed on the hosted big prompt in [CopilotKit/website#445](https://github.com/CopilotKit/website/pull/445) — default to driving, one batched authorization, harness-aware capability request. Both surfaces now say the same thing, which was the point of keeping them in step. Worth noting for reviewers: this takes Atai's side on *"where the skill asks permission to proceed, just do it"*, which was only half-applied before. It also reverses my own per-step confirmations from the first pass on #445 — the dogfooding run is the reason. |
||
|
|
7c5468e5d1 |
fix(skills): ask for the decisions a driving agent must not invent
Batching the Phase 0 authorization removed the per-goal confirmations that were
accidentally serving as decision points. Nothing then asked the developer for the
inputs the agent cannot legitimately choose, and Phase 1 still said "Enter a
Display name" in the imperative — so an autonomous run named the bot itself.
That name is the expensive one. The wizard derives the Channel Code from it, the
Code is what createChannel({ name }) declares and what the developer types as
/invite, and Slack bot names are workspace-wide — Phase 1 already warns that a
collision blocks the install. An agent that settles it has named someone's bot for
them and can fail the install doing it.
Phase 0 now gathers four decisions in one exchange before any browser opens: the
display name, the workspace, the test channel, and whether this is throwaway.
Phase 1 consumes the chosen name instead of inventing one, Phase 1's workspace step
uses the named workspace, and Phase 2's invite names the agreed channel and says
the developer runs it.
States the rule the whole design turns on: the decisions are inputs you cannot
invent, the authorization is permission you need once, and collapsing the second
does not license skipping the first.
|
||
|
|
7193856d96 |
fix(runtime): ignore late lock renewal failures (#6369)
## Summary - stop handling in-flight lock renewal failures after the run has settled - keep aborting when a renewal fails during an active run - cover the completion-before-renewal race with a regression test ## Why Intelligence releases the thread lock after it accepts a terminal run event. A renewal that was already in flight can then return a 409. Clearing the interval stops future renewals, but it does not cancel that pending promise, so Runtime logged an error and called `abortRun()` after the run had completed. The lifecycle guard makes that late rejection a no-op. Active-run renewal failures still follow the existing abort path. ## Testing - `pnpm nx test @copilotkit/runtime` — 1,866 tests passed - `pnpm nx run @copilotkit/runtime:check-types` - `pnpm nx build @copilotkit/runtime` - `pnpm exec oxlint packages/runtime/src/v2/runtime/handlers/intelligence/run.ts packages/runtime/src/v2/runtime/__tests__/intelligence-lock-heartbeat.test.ts` - `pnpm exec oxfmt --check packages/runtime/src/v2/runtime/handlers/intelligence/run.ts packages/runtime/src/v2/runtime/__tests__/intelligence-lock-heartbeat.test.ts` The pre-commit hook also passed affected tests, `publint`, and `attw`. The repo-wide `pnpm check-format` still reports 25 unrelated files already present on `main`; both changed files pass the focused format check. |
||
|
|
ae0dbc2fd7 |
feat(skills): make setup-slack-channel drive the browser by default
Retesting the Slack setup end to end found the skill too cautious to be useful. It stopped at almost every step, where the earlier version stopped only for passwords and got through — and the run was rescued by the developer telling the agent outright to just control their browser. A run that pauses at every control is slower than the manual path it replaced. Three changes, all pulling the same direction. Driving the browser is now stated as the default rather than left implicit in "most of this workflow happens in a browser". When the agent has no browser tool it asks the developer to install one before starting, and names the route for the harness it is actually running in — Claude Code and Codex enable this differently, most other harnesses want a browser-use MCP server — with an instruction to look it up rather than guess. The manual walkthrough survives only for an explicit decline. Consent is batched into one Phase 0 authorization naming the whole sequence: the Slack app from the wizard manifest, its install, the Channel, the adapter attach, and the API key. Phase 3 and the Intelligence reference previously required "state what you are about to change, get an explicit yes" for every dashboard goal, and the reference said it twice. Reading the page before acting stays required; it is no longer a reason to stop. The secret boundary is untouched: the developer still types the bot token, signing secret, and API key themselves, and those remain the only mandatory stops alongside anything the authorization did not cover. |
||
|
|
22108c0948 |
docs(pydantic-ai): constrain the direct dep, not the transitive one
Follows the maintainer's Correction #2 on issue 6363. An exact version in a docs install command is the same rot as the starlette==0.45.3 pin it replaced: it goes stale silently and nobody re-resolves prose. The 2.22.0 the docs shipped was already a version behind current the day it was written. - docs install lines use pydantic-ai-slim[ag-ui,openai]>=2,<3, which constrains the dep the pages actually care about and fails loudly at the v3 boundary - ag-ui-protocol drops out of the docs lines entirely; no doc snippet imports ag_ui, so naming it there was the transitive-dep noise the correction is about - starlette>=0.46.2 stays, because the v2 snippets import Starlette directly. A floor with no ceiling cannot force a downgrade, so it does not recreate the silent backtrack - examples/showcases/pydantic-ai-todos moves to a range in pyproject.toml and relocks; the uv.lock is what reproduces - examples/canvas/pydantic-ai keeps exact pins: it has no lockfile, so requirements.txt is its only reproducibility artifact Smoke-tested the open question from the issue: starlette 1.x works on pydantic-ai v2. All 8 doc pages pass on 2.23.0 + starlette 1.3.1 and on 2.23.0 + starlette 0.52.1, so Jordan's <1.0 guard can be dropped rather than raised. |
||
|
|
86cd674c7c | fix(runtime): ignore late lock renewal failures | ||
|
|
8e59bfd16b |
feat(skills): add channels-setup pointing at the hosted onboarding guide (#6366)
## Summary The Channels onboarding workflow is served at **https://copilotkit.ai/channels-guide.md**, and every other entry point now copies one line that points there — the docs surfaces in #6357, the website strip in [website#444](https://github.com/CopilotKit/website/pull/444), the README in [channels-sdk#15](https://github.com/CopilotKit/channels-sdk/pull/15). Coding agents reached through a **skill** had no such pointer. They matched `setup-slack-channel`, which is scoped to Slack, to the provider half, and to an OpenTag checkout — so "help me get my agent into Teams" landed on a workflow that does not cover it. `channels-setup` closes that gap as a **pointer, not a copy**. The workflow stays in one place and is corrected there, instead of becoming a seventh surface that drifts against the CLI on its own schedule. ## Verifying the fetch is the substance of the file A thin pointer has one non-obvious failure mode, and it is the reason this skill is more than two sentences: ``` $ curl -sL -o /dev/null -w "%{http_code} %{content_type}\n" https://copilotkit.ai/channels-guide.md 200 text/html; charset=utf-8 $ curl -sL https://www.copilotkit.ai/channels-guide.md | grep -o "<title>[^<]*</title>" <title>Page not found | CopilotKit | ...</title> ``` **A missing guide does not return 404.** The site answers unknown paths with a "Page not found" HTML page under HTTP 200. An agent that keys on the status code gets a 73KB marketing page, concludes the fetch succeeded, and improvises channel setup from memory — which is exactly what the guide's boundaries exist to prevent, and it fails in the most expensive way available here: the project installs cleanly and answers nothing. So the skill checks the **body**: markdown rather than HTML, the guide's `# Build and prove a CopilotKit Channels agent` H1, and five `## Phase` headings. If any fail it stops and hands the user the URL, no matter what the status code said. ## Notes - **`RESERVED_LIFECYCLE_SLUGS` is not optional.** Standalone skills are not generated from `packages/*/skills`, so without the entry `sync-plugin-skills` treats the directory as an orphan and deletes it. Test updated alongside, including the hard-coded `size` (10 → 11). - **No existing skill is modified.** `setup-slack-channel` and `copilotkit-channels` keep their descriptions and bodies. Three skills now match channel work; if that proves too ambiguous in practice, narrowing the other two's `description` frontmatter is a follow-up, not a body rewrite. - **No manifest change.** `plugin.json` and `marketplace.json` do not enumerate skills individually. - No changeset — `packages/**` is untouched. ## Blocked on **The guide is not live yet.** [website#444](https://github.com/CopilotKit/website/pull/444) is still open, so the URL currently serves the 404 page shown above. This skill is inert until that merges and deploys — at which point it works with no further change here. The sentinel means the pre-deploy state is a clean stop rather than a wrong answer, so merging early is safe; it just isn't useful yet. ## Validation - `npx vitest run scripts/__tests__/sync-plugin-skills.test.ts` — 10 passed - `pnpm run check:plugin-skills` — `plugin skill mirror in sync`, exit 0; `skills/channels-setup/` survives the orphan pass - `npx oxfmt --check` on both changed TS files — clean - `npx oxlint scripts/` — 0 errors - `SKILL.md` frontmatter parsed with js-yaml: `name=channels-setup`, `version=1.0.0`, description 660 chars - Soft-404 behaviour confirmed against production with the `curl` commands above 🤖 Generated with [Claude Code](https://claude.com/claude-code) |