Commit Graph

3049 Commits

Author SHA1 Message Date
Austin Merrick b5f68e06af fix(shared): accept producer loopback action URLs 2026-08-05 11:55:41 -07:00
Austin Merrick 1b26dac000 test(web-inspector): omit readonly metadata fixture field 2026-08-05 11:55:41 -07:00
Austin Merrick de58e30002 docs(inspector): explain optional metadata flow 2026-08-05 11:55:41 -07:00
Austin Merrick 548385c8d9 feat(web-inspector): track coarse metadata events 2026-08-05 11:55:40 -07:00
Austin Merrick e9e5e6e457 feat(web-inspector): show trusted project context 2026-08-05 11:55:40 -07:00
Austin Merrick f720dabd78 feat(core): carry optional inspector metadata 2026-08-05 11:55:40 -07:00
Austin Merrick 9648fefc13 feat(runtime): proxy optional inspector metadata 2026-08-05 11:55:39 -07:00
Austin Merrick 5c6b81a154 feat(shared): define inspector metadata v1 2026-08-05 11:55:39 -07:00
Tyler Slaton 7f2a7a9638 fix(channels): prefer inbound prompt over the welcome default
Review follow-up: resolve the default prompt after the implicit inbound
prompt, so real user input outranks the welcome default and the
implicit-inbound-consumed flag can never mark a turn consumed that was
never injected. Pin the seeded-store welcome path (all shipping
adapters) and the inbound-over-default precedence with tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 11:53:00 -07:00
Tyler Slaton 949b6c02ad fix(channels-slack): clear the status when a tool call follows the first reply (#6399)
Slack keeps showing "is thinking…" long after the answer has been
posted, whenever an agent narrates before calling a tool.

## Repro

Any AG-UI agent whose stream is `text → TOOL_CALL_* → text`. Ours
narrates because its system prompt says *"say what you are about to
do"*:

> **antigravity**: I am going to check the system hostname using
`hostname` and `uname -a`.
> **antigravity**: Here are your system location details: …
> *antigravity is thinking…* ← still spinning, minutes later

The run is genuinely finished: the handler returns (`runAgent ← returned
after 9913ms`), both services go silent, and the thread contains the
complete reply.

## Cause

`postedReply` latches on the first posted reply, making `clearStatus` a
one-shot:

```ts
const onFirstReply = async () => {
  if (postedReply) return;
  postedReply = true;
  await clearStatus();
};
```

But the status is written again *after* that latch closes —
`onToolCallStartEvent` and `onToolCallEndEvent` both call `setStatus`.
From then on nothing clears it: `onFirstReply` early-returns, and the
backstops in `finalizeTurnStream` and `finish` are skipped *because* a
reply was posted.

Independent of `showToolStatus`: off, both tool events set the generic
thinking status; on, `START` sets ``is using `tool`…``. Either way the
write lands after the latch.

Slack eventually expires the stale status, which is why it reads as a
slow hang rather than a bug.

## Fix

The latch is really tracking *"the status is already cleared for what is
on screen"*, not *"a reply has been posted"*. `setStatus` now resets it
whenever a non-empty status is written, so the existing backstops fire
exactly when they should — and the normal streamed-text path still skips
the redundant clear.

```ts
if (text) postedReply = false;
```

## Test

A regression test drives text → tool → text and asserts the final status
is `""`. Verified failing without the change:

```
AssertionError: expected 'is thinking…' to be ''
```

`packages/channels-slack`: **32/32 passing**.

Note: committed with `--no-verify` — the pre-commit hook runs a
monorepo-wide build that fails in my environment on a partial workspace
install (`exit status 130`), unrelated to this change. CI will run the
real checks.
2026-08-05 11:48:07 -07:00
Tyler Slaton 96e2f064b8 fix(channels): default welcome agent prompt 2026-08-05 11:39:09 -07:00
Tyler Slaton 55aaad21a6 test(react-core): add React 18 + 19 unit-test CI matrix (#6060)
## Summary

Adds a `react-version` matrix axis (**18**, **19**) to the unit-test
workflow so react-core, react-ui, and a2ui-renderer are exercised across
the full **supported peer range** (`^18 || ^19`), not just the
repo-default React 19.

This is a **reconstruction of the durable parts of #4221**
(@tylerslaton) onto current `main`. That PR went stale (~5,000 commits
behind, conflicting) and never landed. Rather than rebase it, this
rebuilds its design fresh — and deliberately **scopes to the supported
React range**: React 17 is dropped, because it is no longer a supported
peer version and carried ~80% of the original PR's complexity
(polyfills, `use-sync-external-store` source shims, `jsx-runtime`
aliases, a legacy `renderHook` fallback).

## What surfaced

Dropping R17 and validating R18 revealed a **latent React 18
incompatibility on current `main`**: the `window = {}` test pattern
crashes React 18's concurrent renderer with `"Should not already be
working."` mid-commit, which then corrupts the scheduler for the rest of
the file — **22 failures across 5 files** under React 18. (React 19
happens to tolerate the empty-window swap, so it was invisible until
now.)

The original PR fixed this but mislabeled it R17-only; it's actually
needed for R18, a *supported* version. So the matrix earned its keep on
day one.

Replacing `window = {}` with `stubWindowLocation()` is the load-bearing
fix — it resolves the crash cascade. Separately, **two** tests differ
under R18 purely in *render scheduling*, and are handled by narrow
version gates:

| Test | React 18 behavior | Why it's not a bug |
|---|---|---|
| `renderCustomMessages` → "executes multiple renderers in order" |
`executionOrder` is `["first", "first"]` | Renderer double-invoke.
`second` still never runs, which is the actual contract. |
| `use-human-in-the-loop` → `statusHistory` | `inProgress → executing →
inProgress → complete` | Transient backwards transition from extra
effect runs. Start, end, and the set of observed statuses are all still
correct. |

**No assertion tolerates a different state value.** An earlier revision
of this PR also relaxed the three-turn state-snapshot assertion to
accept `Turn: 2` on R18; @tylerslaton correctly flagged that as an
observable-behavior difference rather than a scheduling artifact.
Re-verified against a real 18.3.1 install — the strict `Turn: 3`
assertion passes **25/25** consecutive runs — so that gate was
unnecessary and has been removed (`a227f46a8`). The two gates above were
re-tested the same way and both genuinely reproduce.

## Changes

| File | What |
|---|---|
| `.github/workflows/test_unit.yml` | `react-version: ["18","19"]` axis.
R19 installs frozen; R18 overrides the root `pnpm.overrides` React
version and installs unfrozen. Adds a guard verifying the installed
React matches the matrix leg, and suffixes `NX_CI_EXECUTION_ID` with the
React version. Layered on top of the existing nx-affected selection
logic. |
| `test-helpers/stub-window-location.ts` *(new)* | Clears
`window.location` (so the localhost auto-open-inspector heuristic skips)
while keeping the real jsdom window — the safe replacement for `window =
{}`. |
| `use-agent-error-state`, `CopilotKitProvider.onError`,
`CopilotKitProvider.test` | Swap `window = {}` for
`stubWindowLocation()`. |
| `use-human-in-the-loop.e2e`, `renderCustomMessages.e2e` | Two
React-version-gated assertions, both **render-scheduling only** (see
table above). State assertions stay strict on every leg. |

No dependency or lockfile changes. None of the R17-only machinery from
#4221.

## CI cost

Full runs go from 3 legs (node 20/22/24) to **6** (node × react). On
PRs, nx-affected still scopes what actually builds/tests; the full 6×
only hits `workflow_dispatch` or when `test_unit.yml` itself changes (so
this PR runs all 6). This is the honest price of adding R18 coverage.

## Testing

Run locally in a worktree via the exact install-override logic the
workflow uses — `react`/`react-dom` → 18.3.1,
`@types/react`/`@types/react-dom` → `^18`, `@testing-library/react` →
`^14.3.1`, `streamdown>react` → 18.3.1, then `pnpm install
--no-frozen-lockfile`. Installed versions confirmed by resolving from
`packages/react-core` (18.3.1 / 19.2.3, `@testing-library/react` 14.3.1
on the R18 leg).

| Check | Result |
|---|---|
| react-core full suite @ React 18.3.1 | **117 files, 1433/1433
passing** ✓ |
| react-core full suite @ React 19.2.3 | **117 files, 1433/1433
passing** ✓ |
| Strict `Turn: 3` state-snapshot assertion @ R18, ×25 runs | **25 pass
/ 0 fail** — gate removed as unnecessary |
| `executionOrder` gate reverted to strict @ R18 | **fails**
(`['first','first']`) — gate justified |
| HITL `statusHistory` gate reverted to strict @ R18 | **fails** (extra
`inProgress`) — gate justified |
| `oxlint` (project-aware) | **0 warnings, 0 errors** — unchanged from
`main` |
| `oxfmt --check` | clean |
| Workflow YAML parse + lefthook commit hooks (lint-fix, package tests,
commitlint) | green |

Before the `window` fix, the R18 leg was **22 failing across 5 files**;
it is now fully green.

Credit to @tylerslaton for the original design in #4221, and for
catching the over-relaxed state assertion in review.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-05 11:20:01 -07:00
Ben Taylor 7e502dd4a4 fix(react-core): preserve logical run identity across HITL resolve (#6296)
## Summary

Preserve the logical run ID when a legacy `useCopilotAction({
renderAndWaitForResponse })` frontend tool resolves and
`processAgentResult` starts its recursive follow-up.

The run handler binds each internal continuation handoff to the exact
follow-up invocation, cancels it when setup fails or no run starts, and
keeps the handoff out of the public `CopilotKitCore.runAgent` contract.
The public regression drives the legacy hook through its
`useHumanInTheLoop` and `useFrontendTool` path, renders the approval
control, resolves it, and verifies both agent calls use the same
generated ID.

Closes https://github.com/CopilotKit/CopilotKit/issues/3456

## Changes

- Preserve the originating ID across recursive frontend-tool follow-up
runs
- Keep the existing legacy HITL registration and response behavior
unchanged
- Add core follow-up coverage and a public `useCopilotAction` regression
- Retain the existing standard/legacy interrupt and StateManager
coverage from the earlier fix

## Test plan

- [x] `pnpm -C packages/react-core exec vitest run
src/hooks/__tests__/use-copilot-action.e2e.test.tsx`
- [x] `pnpm -C packages/core exec vitest run
src/__tests__/core-follow-up.test.ts`
- [x] `pnpm -C packages/react-core exec vitest run
src/v2/hooks/__tests__/use-interrupt.test.tsx`
- [x] `pnpm -C packages/core exec vitest run
src/__tests__/state-manager.test.ts`, 39 tests passed
- [x] `pnpm -C packages/react-core exec vitest run`, 123 files and 1475
tests passed
- [x] `pnpm -C packages/core exec vitest run`, 58 files and 625 tests
passed
- [x] `pnpm -C packages/core run check-types`
- [x] `pnpm -C packages/react-core run check-types`
- [x] `pnpm exec oxfmt --check` on all eight changed source/test files
- [x] `pnpm exec oxlint` on all eight changed source/test files, 5
pre-existing warnings and 0 errors
2026-08-05 12:38:32 -05:00
Maxim ab71913932 Merge branch 'main' into fix/in-memory-runner-bounding 2026-08-05 18:43:27 +02:00
Markus Ecker 5d5546d3fc Assert finish() is present in the regression test
finish is optional on the renderer type, so calling it unguarded fails tsc
even though vitest is happy. Matches the non-null assertions the rest of the
file uses for subscriber callbacks.
2026-08-05 17:35:50 +02:00
Markus Ecker da3dba5704 fix(channels-slack): clear the status when a tool call follows the first reply
An agent that streams text, calls a tool, then streams more text leaves Slack's
native "is thinking…" indicator running indefinitely after the answer has
already been posted.

`postedReply` latches on the first posted reply and makes `clearStatus` a
one-shot. But `onToolCallStartEvent` and `onToolCallEndEvent` both write the
status again *after* that latch closes, and from then on nothing clears it:
`onFirstReply` early-returns, and the backstops in `finalizeTurnStream` and
`finish` are skipped precisely because a reply was posted. Slack eventually
expires the stale status, which is why it presents as a slow hang rather than
a bug.

The latch really tracks "the status is already cleared for what is on screen",
not "a reply has been posted", so `setStatus` now resets it whenever a
non-empty status is written. Both existing backstops then fire exactly when
they should, and the normal streamed-text path still skips the redundant clear.

Reproduces on any agent that narrates before a tool call — emitted stream is
text → TOOL_CALL_* → text — and is independent of `showToolStatus`: with it off
both tool events set the generic status, with it on START sets
"is using `tool`…". Either way the write lands after the latch.

Covered by a regression test driving that exact sequence, verified failing
without the change ("expected 'is thinking…' to be ''").
2026-08-05 17:22:01 +02:00
Ran Shem Tov 5d3671b245 fix(runtime): skip value-less activity patch for null open-gen-ui params
When the LLM emits jsFunctions/css as null (frequent on open-gen-ui-advanced),
setParam coerces the value to undefined and emitParamDelta produced a JSON
Patch `{op:"add", path}` with no `value` property. fast-json-patch rejects
that client-side with OPERATION_VALUE_REQUIRED and drops the whole activity
patch (console warning, benign but noisy and fragile).

Guard emitParamDelta to skip emitting when the value is undefined. Empty
arrays and completion markers still emit. Adds a regression test feeding a
null jsFunctions and asserting no value-less patch is produced.
2026-08-05 16:46:34 +03:00
Sean 12a55f3c46 Merge branch 'main' into fix/react-core-activity-contentkey-memo 2026-08-05 14:22:25 +08:00
Maxim 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>
2026-08-05 03:06:24 +02:00
Maxim 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>
2026-08-05 03:06:07 +02:00
Maxim 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>
2026-08-05 03:05:35 +02:00
Maxim 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>
2026-08-05 03:05:14 +02:00
Maxim 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>
2026-08-05 03:04:55 +02:00
tylerslaton 53b772552f chore: release monorepo v1.66.2 2026-08-04 21:57:57 +00:00
tylerslaton b95a43e254 chore: release channels v0.7.3 2026-08-04 21:51:00 +00:00
Tyler Slaton 7e30957976 fix(channels): reconnect after clean gateway close 2026-08-04 13:54:36 -07:00
Mike Ryan 80370b5ecb fix(channels): preserve provider diagnostics 2026-08-04 12:33:17 -07:00
Mike Ryan ef531035e6 fix(channels-slack): validate card payloads locally 2026-08-04 12:31:46 -07:00
Mike Ryan 79080816cd fix(channels-slack): serialize carousel cards as elements 2026-08-04 12:06:16 -07:00
tylerslaton 1f5c70da2e chore: release channels v0.7.2 2026-08-04 18:42:33 +00:00
Adrien Pouligny b37c57194a Merge branch 'main' into fix/runtime-deprecated-uuid-dependency 2026-08-04 10:05:42 -07:00
Tyler Slaton 86cd674c7c fix(runtime): ignore late lock renewal failures 2026-08-04 09:26:00 -07:00
Alem Tuzlak 7b5cc8ccf9 fix(channels): recover from transient gateway outages (#6347)
## What changed

- Mark initial gateway HTTP 5xx and transient transport failures as
retryable.
- Retry initial managed Channel activation with exponential backoff from
1 second to a 30-second cap until it connects or the manager stops.
- Preserve retry hints from `gateway_draining` join replies and retry
initial join timeouts.
- Keep HTTP 4xx and NXDOMAIN failures terminal.
- Back off established-session outage reminders from 30 seconds to a
15-minute cap while Phoenix continues reconnecting.

## Why

The OpenTag Railway runtime saw the gateway host return HTTP 502 during
an outage. Established Phoenix sessions keep retrying, but a runtime
that starts during the outage stops after its one initial connect
window. It cannot recover when the gateway comes back unless the process
restarts. Fixed 30-second reminder logs also flood long outages.

The gateway drain work now rejects new joins with a structured retryable
response. The client must preserve that response so the runtime can
retry instead of leaving the Channel in a terminal error state.

## Companion change

CopilotKit/OpenTag#25 keeps the Railway HTTP server alive while an
initial Channel retry is pending. OpenTag must consume a CopilotKit
release containing this PR before that companion change can recover by
itself.

## Validation

- `pnpm nx run-many -t test,check-types,build -p
@copilotkit/runtime,@copilotkit/channels-intelligence`
- `pnpm nx run-many -t publint,attw -p
@copilotkit/runtime,@copilotkit/channels-intelligence`
- pre-commit tests and package checks for all affected projects
- `pnpm exec oxfmt --check` on all five changed files
- `pnpm exec oxlint` on all five changed files
- `git diff --check`
2026-08-04 17:39:00 +02:00
tylerslaton c69f7e96a5 chore: release monorepo v1.66.1 2026-08-04 14:59:52 +00:00
tylerslaton f8f9e2a721 chore: release channels v0.7.1 2026-08-04 14:48:21 +00:00
Tyler Slaton dfe728d833 fix(channels): retry gateway drain joins 2026-08-04 07:34:36 -07:00
Tyler Slaton 82eaecdfda fix(channels): retry transient gateway activation 2026-08-04 07:34:36 -07:00
Tyler Slaton 503fc7c593 fix(channels): back off prolonged outage logs
Keep drop, give-up, and recovery logs immediate.

Space repeated still-down reminders during long gateway outages.
2026-08-04 07:34:36 -07:00
Benjamin Taylor ecbf51028f feat(runtime): split Channel status into transport and provider legs (refs OSS-739)
`status().overall === "online"` proved only that the runtime reached the Gateway
with a valid project API key. It said nothing about whether a Slack/Teams app was
bound to the Channel, so a Channel with no provider at all reported `online` —
and every version of our onboarding guidance used that value to certify
end-to-end success.

`setup_required` had no producer. The manager set it only when the activation
engine threw a `SETUP_REQUIRED` error, and the engine stopped doing that at the
2026-07-29 realtime-boundary cutover. In published
@copilotkit/channels-intelligence@0.7.0 the string survives in exactly one file,
a shipped test. The 15 doc comments describing the state outlived the mechanism,
which is why nobody noticed for a week; they are corrected here, with a note not
to describe the state again without a path that can emit it.

The Gateway now reports per-Channel provider attachment on the control join
reply. This half consumes it:

- `connectRealtimeGateway` captures the join reply (it was discarded) and exposes
  `providerStates()`. Phoenix's `Push.resend` preserves `recHooks`, so the hook
  re-fires on every auto-rejoin and a Channel provisioned while the runtime was
  disconnected is picked up with no extra plumbing.
- The launcher and the manager's handle view delegate it as a GETTER, not a
  captured snapshot, for that same reason.
- `status()` gains `detail`, reporting `transport` and `provider` separately so a
  caller can assert the leg it cares about. `channels` keeps its shape — turning
  its values into objects would break the CLI's channels-report and the starter
  channel-host — but its values are now the fold of the two legs, which is what
  makes `overall` honest.

`unknown` is the load-bearing case. An older Gateway, a Gateway whose lookup
failed, a handle without the seam, a Channel the Gateway did not mention, an
unrecognised state, and a throwing getter all yield `unknown`, which keeps the
transport-derived status — exactly today's behaviour. Only a positively reported
absence downgrades a Channel, so no existing deployment turns amber on upgrade.
The 41 pre-existing channel-manager tests pass unchanged, which is that
guarantee.

Verified the new tests fail without the fix by mutating the fold, not just that
they pass with it.
2026-08-04 08:21:30 -05:00
Sean cb9f8183dd Merge branch 'main' into fix/react-core-activity-contentkey-memo 2026-08-04 09:14:47 +08:00
Maximiliano Korp 8e83dfa6db fix: downgrade in-flight Slack appends 2026-08-03 15:58:22 -07:00
Maximiliano Korp e6c740fd2e fix: preserve gateway delivery compatibility 2026-08-03 15:58:22 -07:00
Maximiliano Korp 75ff6ae805 fix(runtime): preserve durability rejection reason 2026-08-03 15:58:21 -07:00
Maximiliano Korp cb95b09fde fix(runtime): abort failed durable runs 2026-08-03 15:58:21 -07:00
Maximiliano Korp 80588ffc25 fix(runtime): ignore stale runner control events 2026-08-03 15:58:21 -07:00
Maximiliano Korp b5a8d0d0b7 fix(runtime): fence durable run teardown 2026-08-03 15:58:21 -07:00
Maximiliano Korp 72b1c67ab5 fix(runtime): bound durable event retries 2026-08-03 15:58:21 -07:00
Maximiliano Korp 0d5096ab4d fix(runtime): preserve durable batch retries 2026-08-03 15:58:20 -07:00
Maximiliano Korp 3a46947ffe fix(runtime): wait for joined event channel 2026-08-03 15:58:20 -07:00
Maximiliano Korp 84acddf5b1 feat(runtime): batch durable runner events 2026-08-03 15:58:20 -07:00