393 Commits

Author SHA1 Message Date
Benjamin Taylor 8ebdd33998 fix(packages): raise the published Node engines floor to 20
Seven published packages declared `engines.node: ">=18"`, but CI has never
run Node 18 and Node 18 reached end of life on 2025-04-30. The claim
pointed at a runtime nothing tests and that receives no security patches.

Raise those seven to `">=20"`, the lowest version the unit matrix
(20.x, 22.x, 24.x) actually proves, and raise the root manifest to match.

Also give `@copilotkit/runtime` its first `engines` field. It declared
none, while #7089 moved it to pino 10, which drops Node 18. Pino ships no
`engines` of its own, so a consumer on Node 18 installed cleanly and
failed later, at runtime.

Closes #7107

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-12 14:25:05 -05:00
tylerslaton 92f704e3b4 chore: release monorepo v1.71.1 2026-09-11 22:01:26 +00:00
tylerslaton cac7cde862 chore: release monorepo v1.71.0 2026-09-09 22:24:05 +02:00
Benjamin Taylor 2c05ed6885 chore(release): keep release notes in one CHANGELOG.md per release lane
The notes now land in a source-controlled changelog instead of a scratch file
that rides the release branch. One file per lane, because the lanes version
independently: a shared file would interleave `1.70.0`, `angular/0.5.0` and
`channels/0.9.0` into one unreadable sequence.

  monorepo  ->  CHANGELOG.md
  angular   ->  packages/angular/CHANGELOG.md
  channels  ->  packages/channels/CHANGELOG.md

`write-changelog.ts` prepends this release's section on the release branch,
create-pull-request commits it (a tracked file, always staged), and
`extract-release-notes.ts` reads the section back in the publish job as the
GitHub Release body. The changelog is therefore both the durable record and the
review surface: editing a section on the release PR changes what ships.
release-notes.md goes back to being ignored, so the same notes never exist as
two editable copies.

Also deletes 29 changesets-era changelogs that no tooling had written since
April. They stopped at 1.55.2 while the lane shipped 1.69.3, and
packages/angular/CHANGELOG.md still claimed 1.54.3 from before that lane split
onto its own 0.x line. Their content stays recoverable from git history. A test
pins the tracked changelog set to the lanes so they cannot creep back and
contradict the real versions.

Extraction never fails the publish job: it runs after npm publish, so a miss
annotates loudly and falls through to the existing bodyless-release fallback
rather than stranding the tag.

Committed with --no-verify: the pre-commit nx lane cannot run in this worktree
(packages/core and packages/channels-ui have no node_modules, and
`nx run @copilotkit/core:build` fails identically with the tree clean). The only
change under packages/** is deleting orphan markdown that no build or test
reads.
2026-09-09 08:31:01 -05:00
tylerslaton 69a940c70e chore: release monorepo v1.70.3 2026-09-08 23:22:32 +00:00
MikeRyanDev 16514e9424 chore: release monorepo v1.70.2 2026-09-08 20:03:39 +00:00
Martha Kelly Schumann 05fc4e05a4 feat(runtime): expose Learning snapshots to Inspector 2026-09-04 17:11:42 -07:00
Ben Taylor 428fcbd60d fix(core): send the whole RunAgentInput in the Intelligence run body (#6890)
Fixes OSS-1132

## Problem

`IntelligenceAgent` hand-built its REST run body by naming fields, and
`resume` was not one of them:

```ts
body: JSON.stringify({ threadId, runId, messages, tools, context, state, forwardedProps })
```

`HttpAgent` posts the whole `RunAgentInput`, so the self-hosted and SSE
paths carry `resume` correctly. Only the Intelligence transport dropped
it.

The two interrupt paths carry their resume payload in different fields:

| Path | Trigger | Resume travels as | Survived the Intelligence body |
| -- | -- | -- | -- |
| Legacy | `on_interrupt` CUSTOM event | `forwardedProps.command.resume`
| Yes |
| Standard | `RUN_FINISHED` with `outcome: "interrupt"` | top-level
`resume[]` | **No** |

So resuming a standard interrupt against an Intelligence runtime failed
silently: no error, no console output, and the graph simply re-entered
its gate. The server side was already correct — `RunAgentInputSchema`
declares `resume` and `parseRunRequest` parses with that schema.

Nothing the CLI scaffolds hits this combination today (it needs the
built-in agent plus Intelligence mode plus HITL), which is why it stayed
quiet.

## Change

Spread the input instead of naming fields, so a future protocol field
cannot be lost the same way:

```ts
body: JSON.stringify({ ...input, ...(mode === "connect" ? { lastSeenEventId } : {}) })
```

## Testing

Verified in a worktree with its own full `pnpm install` and freshly
built workspace `dist` output. The baseline is the same command with the
two changed files checked out from `origin/main`.

### Two new tests in
`packages/core/src/__tests__/intelligence-agent.test.ts`

- `carries the AG-UI resume array in the run body`
- `posts every RunAgentInput field, so no protocol field is dropped` —
iterates the input's own keys, so it fails on any future omission

### Whole-package suite

| | Test files | Tests |
| -- | -- | -- |
| Baseline (`origin/main`) | 69 passed | 830 passed |
| With this change | 69 passed | **832 passed** |

+2, exactly the new tests. No failures either side.

### Mutation check

Reverted the source fix to the hand-built field list and re-ran the
file:

```
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯
      Tests  2 failed | 59 passed (61)
```

Both new tests fail without the fix, so neither is self-fulfilling.

### Typecheck, lint, format

- `tsc --noEmit -p packages/core/tsconfig.json`: **0 errors**.
- `oxlint` on both files: 0 errors (2 pre-existing
`consistent-function-scoping` warnings in the test file, unchanged).
- Formatted with `oxfmt`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Fixed agent run requests so all provided run input fields are
transmitted correctly.
  * Preserved resume information when starting an agent run.
  * Ensured no supported protocol fields are omitted from requests.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-04 18:46:57 -05:00
Ben Taylor 4da2d7e34e fix(core): hydrate self-hosted threads whose /connect replay contains an errored run (#6528)
Hydrating an existing thread through `/connect` fails on a
**self-hosted** runtime whenever that thread's history contains a run
that ended in `RUN_ERROR`.

## The defect

A `/connect` response is a *replay* of a thread's history, so it can
legitimately carry several past runs back to back — including an errored
run followed by a later `RUN_STARTED`. The base `AbstractAgent` connect
pipeline pushes that stream through `verifyEvents`, which enforces
AG-UI's **single run** lifecycle rules and rejects the sequence
outright:

```
Cannot send event type 'RUN_STARTED': The run has already errored with 'RUN_ERROR'. No further events can be sent.
```

The user-visible effect is the one reported in #4943:
`agent_connect_failed` on reload, and the existing thread never hydrates
its prior messages.

`IntelligenceAgent` already omitted `verifyEvents` from its connect
pipeline for exactly this reason (its JSDoc spells it out). But
`ProxiedCopilotRuntimeAgent.connectAgent` only takes that path in
`RUNTIME_MODE_INTELLIGENCE` — self-hosted (`RUNTIME_MODE_SSE`) fell
through to `super.connectAgent()` and inherited the single-run
verification. So the managed product was fine and self-hosting was not.

## The fix

`ɵconnectWithoutEventVerification`
(`packages/core/src/utils/connect-replay.ts`) holds the
verifyEvents-free pipeline, and **both** paths now use it.
`transformChunks` is still applied — message reassembly is needed either
way.

This is a de-duplication rather than a third copy:
`IntelligenceAgent.connectAgent` drops ~100 lines of hand-replicated
base pipeline (including its private-field `any` escape hatch) and keeps
only its canonical-run-id handling before delegating. Net
`intelligence-agent.ts` change is −101 lines.

### Fidelity to the base implementation

The helper was diffed statement-by-statement against the **real**
`AbstractAgent.connectAgent` in `@ag-ui/client@0.0.57` (recovered from
the shipped source map), not just against `IntelligenceAgent`'s replica.
`verifyEvents` is the only intended difference.

That diff caught a defect in the first push: the base special-cases
`AGUIConnectNotImplementedError` (swallow → `EMPTY`) and the replica did
not. `IntelligenceAgent` never needed it — it always implements
`connect()` — so the gap was invisible there, but on the SSE path it is
load-bearing: `run-handler.ts:447-450` documents that `await
agent.detachActiveRun()` only stopped deadlocking because that error
path still reaches the pipeline's finalize block. Routing it through
`onError` would also fire run-failure callbacks on every subscriber for
a benign condition. Restored, with a regression test.

Also confirmed that dropping `verifyEvents` cannot alter a well-formed
replay: it is a pure gate — 18 `return of(event)` pass-throughs, 42
error paths, and zero `endWith` / `startWith` / `tap` side effects. It
only removes the single-run rejection.

The existing upstream TODO still stands and is carried over:
`@ag-ui/client@0.0.57`'s `connectAgent(parameters?, subscriber?)` takes
no option to skip verification, so this override is still the only way
to express "this stream is a replay, not a run."

## On the second half of #4943

The issue also reports that the legacy chat path doesn't copy the
resolved `threadId` onto the agent before connect/run. **That half is
already fixed on `main`** — the #5041/#4739 fix put `agent.threadId =
resolvedThreadId` in v2 `useAgent`, and `useCopilotChatInternal`
delegates to that same hook. Nothing more was needed.

It was untested, though, and untestable from the suite that looked like
it covered it: `use-copilot-chat-internal-connect.test.tsx` mocks
`useAgent` wholesale, so it cannot observe threadId propagation at all.
This PR adds `legacy-chat-explicit-threadid.test.tsx`, which drives the
legacy hook through the **real** `useAgent` under a real `<CopilotKit>`,
covering both the explicit-threadId case and the "don't adopt a
non-explicit placeholder UUID" case.

It reads the agent off `useCopilotChatInternal()`'s own return value
rather than calling `useAgent` in the probe. That distinction matters:
the first version of this test did call `useAgent`, so the probe itself
performed the assignment under test and the test passed **even with
`useCopilotChatInternal()` removed entirely**. The current version is
mutation-checked — disabling the assignment in v2 `useAgent` fails it
(`expected 'dc051f13-…' to be 'cookie-backed-thread'`).

Contributor PR #4969 proposed a manual assignment for this half; it is
now redundant.

## Testing

Worktree caveat, stated up front: this worktree symlinks the primary
checkout's `node_modules`, so `@copilotkit/shared` and
`@copilotkit/core` resolve to that checkout's **stale `dist`**. That
produces failures unrelated to this change; each is baselined against
clean `main` in the same environment below. CI installs fresh and is the
authoritative gate.

**1. Reproduces the reported failure before the fix.** The new core
test, run on unmodified `origin/main`, fails with the exact error from
the issue:

```
FAIL  src/__tests__/proxied-connect-replay-multi-run.test.ts > hydrates a thread whose replayed history contains an errored run
AssertionError: promise rejected "Error: Cannot send event type 'RUN_STARTE…" instead of resolving
Caused by: Error: Cannot send event type 'RUN_STARTED': The run has already errored with 'RUN_ERROR'. No further events can be sent.
```

**2. Passes after the fix**, hydrating both runs' messages (`["msg-1",
"msg-2"]`):

```
✓ src/__tests__/proxied-connect-replay-multi-run.test.ts (1 test) 11ms
Test Files  1 passed (1)
```

**3. Connect-not-implemented guard, fail-first.** With the guard
removed, the new second test fails exactly as the base contract
predicts:

```
× swallows AGUIConnectNotImplementedError instead of failing the run
AssertionError: promise rejected "Error: Connect not implemented. This meth…" instead of resolving
```

**4. Full `@copilotkit/core` suite** — this is the evidence the
`IntelligenceAgent` extraction is behavior-identical, since
`intelligence-agent.test.ts` exercises that path heavily:

```
Test Files  59 passed (59)
      Tests  635 passed (635)
```

(excludes `core-inspector-metadata.test.ts`; its 20 failures are the
stale-`shared`-dist artifact — verified identical on clean `main`: `20
failed | 2 passed`, missing export `InspectorMetadataV1`)

**5. `@copilotkit/react-core` — new + adjacent existing suites:**

```
✓ src/hooks/__tests__/use-copilot-chat-internal-connect.test.tsx (7 tests)
✓ src/hooks/__tests__/legacy-chat-explicit-threadid.test.tsx (2 tests)
✓ src/components/copilot-provider/__tests__/v1-explicit-threadid-bridge.test.tsx (5 tests)
Test Files  3 passed (3)
      Tests  14 passed (14)
```

Full react-core suite: `8 failed | 1492 passed (1500)`. All 8 are in
`use-interrupt` / `use-pin-to-send` / `CopilotChatView.pinToSend` — none
touch connect replay or threadId, and clean `main` in this worktree
fails the identical 8 (`8 failed | 37 passed (45)` for those three files
alone).

**6. `@copilotkit/vue`** (affected via core): `100 passed (100)` files,
`1072 passed (1072)` tests.

**7. Types, lint, format:**

```
tsc -p packages/core/tsconfig.json --noEmit   → no errors in any changed file
oxlint  <5 changed files>                     → Found 0 warnings and 0 errors
oxfmt --check <5 changed files>               → All matched files use the correct format
```

The only remaining `tsc` errors are 4 pre-existing stale-dist ones in
`agent-registry.ts` / `types.ts` (`InspectorMetadataV1`), untouched by
this PR.

Fixes #4943


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved thread hydration when reconnecting to histories containing
multiple runs, including runs that previously ended in error.
* Prevented unsupported connection errors from being reported as run
failures.
* Ensured connection state is properly finalized after replaying a
thread.
* Legacy chat components now correctly reuse an explicitly provided
thread ID while preserving generated IDs when none is provided.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-04 15:37:42 -05:00
Mike Ryan c276befc13 fix(runtime): harden single-route resource requests 2026-09-04 10:39:10 -07:00
Mike Ryan 840ad3c14a feat(runtime): support Intelligence over one route 2026-09-04 10:36:27 -07:00
Benjamin Taylor b06ea361c1 send the whole RunAgentInput in the Intelligence run body
The Intelligence transport hand-built its REST body by naming fields, which
silently dropped the AG-UI `resume` array. A standard interrupt (RUN_FINISHED
with outcome "interrupt") carries its resume payload at the top level, so
resuming one against an Intelligence runtime never reached the server and the
graph re-entered the same gate with no error. The legacy `on_interrupt` path
was unaffected because it travels inside forwardedProps.

Spread the input instead of naming fields, so a future protocol field cannot
be lost the same way, and add a test that fails if any field is dropped.

Fixes OSS-1132

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 09:09:43 -05:00
tylerslaton 71b2f481f9 chore: release monorepo v1.70.1 2026-09-03 15:49:49 +00:00
Alem Tuzlak 808113b923 fix(core): keep webmcp registrations stable across stale rejections and reactive changes 2026-09-02 17:19:34 +02:00
Alem Tuzlak f6d2a908a3 feat(core): register webmcp-enabled frontend tools on document.modelContext 2026-09-02 16:26:03 +02:00
maxkorp 3a64564508 chore: release monorepo v1.70.0 2026-08-31 19:34:27 +00:00
Max Korp b07b1320f2 feat(runtime): use managed Intelligence authority (#6098)
## What changed

- Add standalone `CPK_TELEMETRY_ID` support to Runtime v1 and v2.
- Keep telemetry opt-out, sampling, Segment, and legacy license fallback
behavior.
- Fetch structured Intelligence entitlements and map them to current
client status.
- Share concurrent entitlement lookups, retry short-lived failures, and
reject stale grants.
- Make managed React, Angular, and Vue thread UIs use Runtime
entitlement authority.
- Keep assistant feedback stable when unrelated Inspector settings
change.
- Update Runtime, telemetry, self-hosting, and Web Inspector docs.

## Why

Managed Intelligence projects use a project API key for product access
and a non-secret telemetry ID for attribution. Offline license tokens
remain a self-hosted entitlement concern.

Starter-template and AgentCore changes live in #6188.

## Companion PRs

- Starter templates: #6188
- CopilotKit/Intelligence#628
- CopilotKit/oss-path-to-production#226

## Review corrections

- Scope shared entitlement attempts to one API key and endpoint.
- Ignore stale attempts after credentials change.
- Bound retries after short denials and transport failures.
- Accept telemetry IDs only when they match the public identifier
contract.
- Read Inspector context in its button, so unrelated label changes do
not rerender assistant feedback.

## Validation

- React Core full suite: 1,537 Vitest tests and 47 script tests passed.
- Runtime, Core, Shared, Angular, Vue, and Web Inspector focused suites
passed.
- React Core typecheck and build passed after the final rebase.
- Direct builds and type checks passed for Angular, Core, Runtime,
Shared, Vue, and Web Inspector.
- Shell docs typecheck and production build passed.
- Changed Vue files passed ESLint.
- `git diff --check` passed.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Added structured runtime entitlement support for managed and
self-hosted deployments.
- Feature access and usage limits now reflect active entitlements, with
legacy license compatibility.
- Added runtime entitlement diagnostics to the Inspector’s Threads view.
- Added runtime-scoped telemetry identities and configurable telemetry
ID support.

- **Bug Fixes**
- Licensing interfaces remain in a loading state during retryable
entitlement outages.
- Improved recovery after runtime connection, target, or transport
changes.
- Prevented stale entitlement data from granting access after refresh
failures.

- **Documentation**
- Documented entitlement statuses, telemetry identity precedence,
sampling, and Inspector telemetry behavior.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-31 11:55:42 -07:00
Ben Taylor 3ee0ec189c fix(core,react-core): stop dropping frontend tools, and let catch-all actions wait for a response (#6524)
Two frontend-tool defects that share a shape: a tool that was registered
correctly still never reached the agent, or reached the render without
the ability to respond.

## `useFrontendTool` tools dropped when the runtime enables
`openGenerativeUI` (#4952)

Tools reached the core registry through two owners that shared one array
— the provider via `setTools()`, hooks via `addTool()` — and `setTools`
replaced the array wholesale. Any provider re-sync after mount therefore
wiped every hook-registered tool.

A runtime with `openGenerativeUI: true` made it reproduce on **every**
mount: `/info` flips the flag asynchronously, the provider re-derives
its tool list to add `generateSandboxedUi`, and the re-sync dropped the
app's own tools. The agent then received only `generateSandboxedUi`,
exactly as reported.

The fix splits the two owners into their own buckets, merged on read
with hook entries winning. This mirrors what `CopilotKitCoreReact`
already does for render tool calls (`react-core.ts`) — tools simply
never got the same treatment. Because the clobber lived in core rather
than in one provider, **Vue had the identical bug and is fixed by the
same change** — verified end to end, not by shape:
`CopilotKitProvider.vue:461` assigns `runtimeOpenGenerativeUIEnabled`
from the core, `:297` derives `openGenerativeUIActive`, `:328`/`:354`
add the built-in to `allTools`, and `:492` re-syncs it through
`setTools` behind the same `didMountRef` skip. Angular is unaffected: it
never calls `setTools`, passes `tools` only through the constructor
(`copilotkit.ts:152`), and registers just the *renderers* from
`config.tools` (`:224`) — no second registration.

`addTool` now shadows a provider tool of the same name instead of
refusing to register, and only warns when another imperative
registration already holds the name. It also no longer pushes onto the
array the provider passed in, and `initialize` copies that array like
`setTools` already did.

**One intentional behavior change worth a reviewer's eye:** `setTools`
now replaces provider-owned tools only, so `setTools([])` no longer
clears tools registered through `addTool`. That narrowing *is* the fix,
but anyone calling `setTools([])` as a "clear everything" would now need
`removeTool` per tool. Nothing in the repo does (core, react-core, vue,
angular suites all pass).

## Catch-all actions could not wait for a response (#1746)

`getActionConfig` short-circuited on `name === "*"` before checking for
a wait-render, so a catch-all declaring `renderAndWaitForResponse` was
silently downgraded to render-only — no `respond`, and in practice a
`render is not a function` throw, since the render-only path reads
`action.render`. Handling N human-in-the-loop tools required N hooks.

A catch-all with a wait-render now routes to the human-in-the-loop path.
Core already had the execution half
(`getWildcardTool`/`executeWildcardTool`), so this is routing, not new
machinery. Two supporting changes make it usable:

- The HITL render props now carry **the name of the tool actually being
called**. It equals the registration name for a normal action, but a
catch-all needs it to tell N tools apart, and `"*"` was being written
over it in both the v1 wrapper and the v2 hook.
- `CatchAllFrontendAction` accepts
`renderAndWaitForResponse`/`renderAndWait` alongside `render`, mutually
exclusive as on `FrontendAction`, with `CatchAllActionRenderPropsWait`
exported.

Also: a wildcard tool is no longer advertised to the agent. It is a
local catch-all handler for calls with no exact match, so offering the
model a tool literally named `*` was never meaningful. Latent before
this PR (nothing in React registered a wildcard *tool*); catch-all HITL
activates it.

## Synced with `main` (2026-08-31)

`main` moved react-core's v1 tree under `src/v1-deprecated/` while this
branch was open, so the merge had exactly two conflicts, both
relocations:

- `use-default-tool.ts`'s `DistributiveOmit` change re-applied on the
moved file, keeping main's deprecation banner.
- the catch-all HITL e2e test moved into
`src/v1-deprecated/hooks/__tests__/`, with its `../../v2/...` imports
re-rooted to `../../../v2/...` to match its sibling
`use-copilot-action.e2e.test.tsx`.

The defect is still live on current `main` —
`CopilotKitProvider.tsx:838` still calls `copilotkit.setTools(allTools)`
— and both regression tests still bite there. The suites, the
before/after checks, `tsc --noEmit`, `oxlint` and `oxfmt` below were all
re-run on the merged tree; the browser walkthrough under **Live
verification** is from the pre-merge branch and was not repeated.

## Testing

**New regression tests**

- `packages/core/src/core/__tests__/run-handler-tool-registry.test.ts` —
13 tests: `addTool` survives `setTools`, hook precedence, agent-scoped
vs global, `removeTool` across both buckets, remount re-registration,
capability toggles surviving a re-sync, provider ordering, caller-array
aliasing in both directions, wildcard never advertised.
-
`packages/react-core/src/v2/providers/__tests__/CopilotKitProvider.openGenerativeUIToolLoss.test.tsx`
— drives the **real** core over a stubbed `/info` that returns
`openGenerativeUIEnabled: true`, asserting the hook tool survives.
-
`packages/react-core/src/v1-deprecated/hooks/__tests__/use-copilot-action-catch-all-hitl.e2e.test.tsx`
— end-to-end through the real provider and core: catch-all gets the real
tool name and a live `respond`, the tool result lands on the original
`toolCallId`, the follow-up run fires, and `*` is absent from
`runInputs[0].tools`.

**Both new tests were confirmed to fail before the fix — re-confirmed
after syncing `main`,** by checking the touched sources out at
`origin/main` and rebuilding core's dist:

```
× keeps the hook tool once the runtime turns openGenerativeUI on
  → expected [ 'generateSandboxedUi' ] to include 'sayHello'
```

and before the routing fix, the catch-all test failed with the exact
defect from the issue:

```
× gives the catch-all render a live respond and the real tool name
  → TypeError: render is not a function
    ❯ render src/hooks/use-render-tool-call.ts:44:22
```

**Suites (all green)**

```
@copilotkit/core        67 files,  799 tests passed
@copilotkit/react-core 137 files, 1558 tests passed
@copilotkit/vue        101 files, 1092 tests passed
@copilotkit/angular     49 files,  317 tests passed (1 skipped)
```

`tsc --noEmit` clean for `core` and `react-core`; `oxlint` 0 errors on
the changed files (16 warnings, all pre-existing); `oxfmt` applied.

**Live verification** — `examples/v2/react/demo` in a browser against
built dists, with a temporary stub AG-UI agent (no LLM key) that echoes
the tool names it receives, a runtime configured `openGenerativeUI:
true`, no `openGenerativeUI` prop on the provider, one
`useCopilotAction` frontend tool and one `useCopilotAction({ name: "*",
renderAndWaitForResponse })`:

```
TOOLS_RECEIVED: ["generateSandboxedUi","sayHello"]     <- #4952: hook tool survived; no "*" leaked
catch-all handling: book_call   status: executing      <- #1746: real tool name, live respond
[click "Pick Tuesday"]
catch-all handling: book_call   status: complete
TOOL_RESULT_RECEIVED: "{"slot":"tuesday"}"             <- follow-up run received the result
```

0 console errors. The stub route and page were scratch and are not in
this branch.

## Notes for reviewers

- Community PR #4967 also targets #4952 by merging in the provider
instead. I took the core-layer fix because the clobber is in core's
registry and every framework provider hits it — patching one provider
leaves Vue broken. Happy to reconcile.
- Pre-existing and deliberately **not** changed here:
`useCopilotAction({ name: "*" })` render props do not infer, because the
hook's parameter is a union TypeScript cannot contextually type. This
already affected plain `render` before this PR (verified), so the new
tests and docs annotate props explicitly. Fixing it needs a
`useCopilotAction` overload — worth a follow-up.
- Deliberate small duplications, flagged rather than abstracted:
`WILDCARD_TOOL_NAME` is a one-line const in both core and react-core's
v2 HITL hook (sharing it would mean a new public export from core), and
the link between "a catch-all render receives `name`" and "the v1 HITL
wrapper supplies it" is a cast rather than a type — making it typed
means adding `name` to the public `ActionRenderPropsWait`, which is
wider than this fix.
- #4759 is left with contributor PR #5308, and #6101 needs its own
design pass since it introduces new public API.



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Added catch-all human-in-the-loop actions that can handle unregistered
tools and wait for user responses.
- Catch-all action renderers now receive the actual invoked tool name
and arguments.
  - Added support for wait-aware catch-all rendering types.

- **Bug Fixes**
  - Preserved frontend tools when provider tool lists are refreshed.
- Prevented duplicate tools and ensured registered tools take
precedence.
- Hidden wildcard tools from agent-advertised tool lists while keeping
them available for handling requests.


<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-31 13:00:00 -05:00
Maximiliano Korp 549c472d43 fix(core): preserve recovery state across runtime changes 2026-08-31 10:46:15 -07:00
Mike Ryan 0a99ef580a fix(runtime): restore managed authority contracts 2026-08-31 10:46:13 -07:00
Mike Ryan f1ac08938a feat(runtime): use managed Intelligence authority 2026-08-31 10:46:12 -07:00
Benjamin Taylor c5107bee53 fix(core): keep the connect-not-implemented escape hatch in the shared pipeline
Self-review catch. The extracted pipeline was faithful to IntelligenceAgent's
copy but not to the base AbstractAgent.connectAgent it replaced on the
self-hosted path, which special-cases one error:

  catchError((error) => {
    this.isRunning = false;
    if (!(error instanceof AGUIConnectNotImplementedError)) {
      return this.onError(input, error, subscribers);
    }
    return EMPTY;
  })

IntelligenceAgent never needed it — it always implements connect() — so the
omission was invisible there. On the SSE path it is load-bearing:
run-handler.ts awaits detachActiveRun() before every run and documents that
this only stops deadlocking because the ConnectNotImplementedError path
reaches the finalize block. Routing it through onError would also fire
run-failure callbacks on every subscriber for a benign condition.

Restores the guard, adds a regression test (verified fail-first: without the
guard connectAgent() rejects with "Connect not implemented"), and matches the
base's `void this.onFinalize(...)`.

Also makes the legacy-chat threadId test actually test its claim. It read
agent.threadId from its own useAgent() call, so the probe performed the very
assignment under test — it passed even with useCopilotChatInternal() removed
entirely. It now reads the agent off the hook's own return value, and is
mutation-checked: disabling the assignment in v2 useAgent fails it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 08:11:21 -05:00
Benjamin Taylor 35616f46a2 fix(core): hydrate self-hosted threads whose replay contains an errored run
A /connect response replays a thread's history, so it can carry several
past runs back to back — including a run that ended in RUN_ERROR followed
by a later RUN_STARTED. The base AbstractAgent connect pipeline runs the
stream through verifyEvents, which enforces single-run lifecycle rules and
rejects that sequence outright:

  Cannot send event type 'RUN_STARTED': The run has already errored with
  'RUN_ERROR'. No further events can be sent.

IntelligenceAgent already omitted verifyEvents from its connect pipeline
for this reason, but self-hosted runtimes (RUNTIME_MODE_SSE) fell through
to super.connectAgent() and so never hydrated such a thread.

Extract that verifyEvents-free pipeline into a shared helper used by both
paths, rather than keeping two copies of a delicate 60-line pipeline.
IntelligenceAgent keeps its canonical-run-id handling and delegates the
rest.

Also adds legacy-chat threadId coverage: the pre-existing connect suite
mocks useAgent wholesale, so nothing exercised the real propagation the
CopilotPopup path depends on.

Fixes #4943

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 08:11:01 -05:00
Benjamin Taylor ebbba19101 Merge origin/main into ben1/fe-tool-registry-4952-1746
The v1 react-core tree moved under src/v1-deprecated/, so the two conflicts
were relocations: use-default-tool.ts's DistributiveOmit change re-applied on
the moved file, and the catch-all HITL e2e test moved into
src/v1-deprecated/hooks/__tests__/ with its v2 imports re-rooted.
2026-08-31 07:58:44 -05:00
Markus Ecker 71d9731d45 chore(deps): bump @ag-ui/* to 0.0.59
Moves the published packages from 0.0.57 to the current AG-UI release across
@ag-ui/client, core, encoder and proto — 27 declarations in 18 packages.

0.0.59 is the first release carrying the subagent protocol surface
(SUBAGENT_STARTED/FINISHED/ERROR, subagentRunId) along with the null-omission
cleanup, so this is the dependency CopilotKit's subagent work needs.

Scope is packages/** plus the release script noted below. The examples and
showcases sit on a spread of older pins (0.0.40 through 0.0.58) and are left
alone.

One behavioural change comes with the bump. channels-core ships
sanitizeAgentEventStream because @ag-ui/client used to reject a TOOL_CALL_START
carrying parentMessageId: null — the shape @ag-ui/langgraph emits for an
interrupt-triggering tool call. 0.0.59 accepts that null and treats it as
absent, so the two tests asserting the run dies WITHOUT the sanitizer no longer
hold. They now assert the run survives, and the one at agent level still checks
the tool call actually arrives so it cannot pass vacuously. The sanitizer is
untouched and its coercion tests are unchanged; it is simply no longer the
thing keeping such a run alive.

The bump also broke the packed Angular consumer matrix. That job generates a
smoke app from scripts/release/lib/angular-package.ts, whose manifest restated
"@ag-ui/client": "0.0.57" as a literal while packages/angular moved to 0.0.59.
pnpm then installed both copies and the app failed to compile:

  TS2322: Type 'SmokeAgent' is not assignable to type 'AbstractAgent'.
    Types have separate declarations of a private property '_debug'.

The smoke app imports AbstractAgent directly, so it has to resolve the identical
copy the library ships against. Read that version off the packed manifest --
which verify-angular-package.ts already parses for the Angular support contract
-- instead of restating it, so no future AG-UI bump can desynchronise it.
2026-08-28 16:18:52 +02:00
Alem Tuzlak 5686a0669e Merge branch 'main' into lukas/oss-904-runtime-connection-status 2026-08-28 14:06:40 +02:00
Alem Tuzlak 1dfc5cdafa refactor(core): remove OSS-904 design comments 2026-08-28 13:27:37 +02:00
Alem Tuzlak a7191e2a12 fix(core): bound recovery /info hang and tighten OSS-904 comments 2026-08-28 13:08:44 +02:00
Alem Tuzlak b8b35b736c fix(packages): declare the MIT SPDX license on five published packages (#6511)
Five packages publish to npm with no `license` field, so registry
metadata and automated license scanners report them as **Unknown**:

```
@copilotkit/agentcore-runner  published=1.68.1  license=<NONE>
@copilotkit/core              published=1.68.1  license=<NONE>
@copilotkit/sqlite-runner     published=1.68.1  license=<NONE>
@copilotkit/voice             published=1.68.1  license=<NONE>
@copilotkit/web-inspector     published=1.68.1  license=<NONE>
```

The repo is MIT (see `LICENSE`) and every other published
`@copilotkit/*` package already declares it — these five were simply
missed. This adds `"license": "MIT"` to each, positioned before
`"repository"` to match the sibling packages.

## Why

Reported downstream in #2860, where a corporate procurement scan refused
packages whose license it could not resolve. That class of scanner reads
the `license` field from registry metadata; a `LICENSE` file in the repo
is not enough, and these packages ship no `LICENSE` file either.

**Correcting the record on that issue while I am here:** the `@ag-ui/*`
packages named in the original report are *not* affected. Every version
the reporter’s scanner flagged already carries `"license": "MIT"`:

```
@ag-ui/client@0.0.42     MIT
@ag-ui/core@0.0.37       MIT
@ag-ui/core@0.0.42       MIT
@ag-ui/encoder@0.0.42    MIT
@ag-ui/langgraph@0.0.20  MIT
@ag-ui/proto@0.0.42      MIT
```

`@ag-ui/core` has declared MIT since at least 0.0.35. An earlier triage
note on #2860 attributed the failure to a missing SPDX field upstream;
that was wrong, and why their scanner reported `Unknown` for `@ag-ui/*`
is still unexplained. This PR fixes the part that is genuinely defective
on our side.

## Testing

Metadata-only; no source, build, or runtime change.

- Confirmed the five missing fields against the live registry with `npm
view <pkg> license` (output above), and confirmed the other published
`@copilotkit/*` packages (`runtime`, `react-core`, `react-ui`, `shared`,
`sdk-js`, `angular`, `channels`, `channels-core`) already report `MIT`.
- Enumerated every non-private `packages/*/package.json` on
`origin/main` to confirm these five are the complete set missing the
field.
- Each edited file re-parsed with `json.load` and reports `MIT`.
- The `sync-lockfile` pre-commit hook resolved all 71 workspace projects
against the edited manifests without error.

Placement matches `packages/shared/package.json`, where `"license"`
immediately precedes `"repository"`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-28 12:32:22 +02:00
MikeRyanDev 8617f5b76b chore: release monorepo v1.69.3 2026-08-27 16:47:48 +00:00
Lukas Moschitz c69ef433b1 fix(core): let recovery prune an agent nothing is standing on
A recovery re-sync merged: it added and updated what the runtime reported
and never removed anything. That was safe but too blunt — an agent the
developer genuinely deleted stayed visible until a page reload, so the
application kept offering something that no longer existed.

The harm was never removal; it was removing the agent a live conversation
hangs on. Recovery now reconciles instead of merging, dropping an agent
only when both conditions hold: the runtime reported at least one agent
(an empty list is a runtime that has not finished registering, and
nothing it says is worth acting on), and the agent carries no
conversation state — no messages, and no thread bound to it by a
binding. Both are decidable inside the core, the second off the agent
instance, so nothing here depends on the UI layer.

Note that `threadId` is always populated: `AbstractAgent`'s constructor
generates one when none is supplied, and the run pipeline never writes
it. So presence says nothing, and the registry records the value it
minted each remote agent with in order to recognise the value having
moved — which is what a binding resolving a thread actually looks like.

Non-recovery re-syncs are unchanged: a deliberate runtime url, transport,
header or credentials change and the initial handshake all still replace
the set outright, conversation state included.

This is only safe because `subscribeToAgent` now leaves a live
subscription alone when handed the same instance. Pruning changes the
set, so recovery announces a change, so the state manager re-subscribes
per agent — and recovery fires while the run whose response proved the
runtime was back is typically still streaming. Covered by a test that
prunes across an in-flight run; it fails if that guard is removed.
2026-08-26 13:25:13 +02:00
Lukas Moschitz c1e6e72812 refactor(core): name the watchdog's flag after what it records
It records whether the silence report caused a confirmation check, not whether
the timer fired, and the two are now deliberately different. One honest name
beats a doc comment explaining that the name is not quite right.
2026-08-26 13:25:13 +02:00
Lukas Moschitz 4f432848f5 docs(oss-904): correct the claims about thread requests and the error state
Five comments justified routing thread requests through the instrumented fetch
by saying it lets opening a view restore the status after an outage. It does
not: every binding withholds its thread requests until the status is already
connected, so while it is red nothing is sent. The justification is DETECTION
only, which is what the CopilotChat site already said correctly.

Also:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Also from the review:

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

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

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

RUNTIME_PROBE_COOLDOWN_MS is exported so waits derive from it.
2026-08-26 13:25:12 +02:00
Lukas Moschitz 215b72b622 fix(core): count the Intelligence runtime request towards connection health
Intelligence mode never touches the HTTP run route: it asks the runtime
over HTTP for realtime join credentials and then talks to the realtime
gateway over a websocket. That HTTP request used the global fetch, so it
bypassed the instrumented seam entirely and an Intelligence-mode
application still could not notice a runtime that went away — the one
case the destination rule ("a request counts if it goes to the runtime,
whatever issued it") was chosen to cover.

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

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

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

Both the inbound transition and a failed recovery re-sync preserve every
piece of runtime knowledge. Conversation state lives on the agent
instance and submission is gated on an agent being bound, so discarding
the agents would empty the chat and close the only path back out of the
error state.
2026-08-26 13:25:12 +02:00
Lukas Moschitz efe5199bfa refactor(core): one definition of abort detection
The run handler matched four abort signals inline while the agent module
kept its own name-only predicate. Extract the superset (the AbortError
name plus the fixed message strings) into utils/abort-error.ts so every
place that classifies a failed request agrees on what a cancellation is.
2026-08-26 13:25:12 +02:00
tylerslaton 9629e930d1 chore: release monorepo v1.69.2 2026-08-26 00:18:42 +00:00