The watchdog test cluster in test_entrypoint_watchdog.py had three soundness
defects that let tests pass without exercising real behavior.
1. test_size_pid_reaped_by_in_subshell_trap asserted the literal
trap kill $SIZE_PID string, which exists ONLY in an explanatory comment
describing the old design. The shipped code registers
trap _reap_watchdog_children EXIT, so the test passed even when the real
trap registration was removed. Now asserts the shipped registration on an
executable (comment-stripped) line and that the named handler is defined.
2. test_size_pid_reaped_on_watchdog_exit_behavioral wrote its own mock
watchdog using the older SIZE_PID trap shape and never invoked shipped
code, so it passed even when the shipped reaper was gutted. Now extracts
the real _agent_descendants and _reap_watchdog_children definitions
verbatim from entrypoint.sh and drives them with the shipped order.
3. dummy_was_killed used a single-shot dummy.poll() immediately after the
run returned, racing the asynchronous SIGKILL delivery. Replaced with
bounded _wait_process_exited / _assert_still_alive helpers selected by
return code, making the killed/alive assertion deterministic.
entrypoint.sh unchanged. 22 passed; determinism confirmed by repeated runs.
The langgraph-typescript backend's @langchain/langgraph-api FileSystemPersistence
serialises all accumulated thread/run/checkpoint state to .langgraph_api on a
3-second timer. Under the D6 probe fan-out (36 parallel probes) the dir filled
past the 200MB size-watchdog threshold in ~90s, the watchdog killed the agent,
and on rapid restart the D6 cron refilled and re-tripped it until Railway
crash-loop backoff stopped restarting the container (2026-07-13 outage, staging
and prod).
Mirror PR #5825's langgraph-python fix, which exported
LANGGRAPH_DISABLE_FILE_PERSISTENCE=true so the python inmem runtime skips its
flush-to-disk loop. The TS package has no such switch and its persistence
writers are unexported module singletons behind an exports-map wall, so ship a
node --import preload (src/agent/disable-file-persistence.mjs) that, gated on the
same env var, no-ops node:fs/promises writeFile/mkdir for .langgraph_api paths
while leaving in-memory state (the real runtime state) intact. Wire it into
npm start and export the env var in entrypoint.sh.
Behavior preserved: runs still execute and thread state reads back from the
in-memory checkpointer within the container lifetime; only disk persistence is
removed, so the size-watchdog has nothing to fill and never trips under load.
## Problem
Claude Agent SDK docs could render malformed or missing extracted
snippets across the generated Python and TypeScript integration docs.
The generative UI pages had stale duplicate regions and generic setup
leakage, while the custom look-and-feel reasoning and slots pages
referenced demo cells or regions that did not exist for every Claude
integration.
## Why
The docs pipeline treated accidental duplicate region names across files
as intentional multi-file regions, and several authored docs pages
drifted from the actual generated showcase demo IDs/regions. That left
some pages visually correct at a glance but broken when users opened
specific extracted code snippets.
## Fix
- Move the shared `bar-chart-renderer` regions to the complete
`useComponent` call and delete stale duplicate snippet files.
- Add an explicit duplicate-region guard and verifier coverage for
accidental cross-file region collisions.
- Add line-emphasis support for extracted `<Snippet>` blocks and setup
`<DemoCode>` output.
- Scope generative UI feature pages away from generic `agent-setup`
boilerplate.
- Repair the shared reasoning-messages docs to use the generated
`reasoning-default` and `reasoning-custom` demo cells.
- Add the missing Claude Python chat-slots teaching snippet regions and
keep both Claude slot snippets self-contained.
- Broad-audit both Claude integration docs locally, then targeted-audit
the repaired reasoning/slots pages in light and dark mode.
The aggregate e2e-smoke:<slug> red signal omitted errorDesc on the normal
return path, so an abort/timeout/send-budget-exhausted red that runLevel
RETURNS (not throws) showed on the PRIMARY dashboard tick as an unclassified
content-shaped red — only the side chat:/tools: rows kept the classifier.
Thread the failing level's errorDesc (L3 precedence, L4 fallback) onto the
aggregate so the primary tick matches the side row and the launcher-phase
abort path. Does not change red/green — only carries the classifier.
Also reorder the aborted-and-empty short-circuit ABOVE the alternate-content
/ raw-byte evaluate reads: an aborted run's page is tearing down, so those
reads were swallowed against a dead page and emitted an ambiguous empty
histogram. Non-aborted runs still perform the alternate-content salvage.
A mid-poll abort — the external ctx.abortSignal firing, or the driver's
own hard-timeout landing during the first-token poll — makes runAttempt
return empty WITHOUT throwing. The retry loop breaks and control falls to
the clean-exit path, where the level was misclassified as a generic
content red ("empty assistant response", probe.exit outcome "err", no
errorDesc). That masqueraded a teardown/abort/timeout as a CONTENT
failure on the dashboard + CVDIAG.
Add an aborted-AND-empty guard before the content-red gate that
short-circuits to the same abort classification the other paths use
(errorDesc "abort", probe.exit outcome "timeout"). Discriminator is
abortSignal.aborted, not emptiness alone: a genuinely-completed-empty
turn (not aborted) stays the content-red "empty assistant response".
Harmonize the three readTurnState() consumers in the d4 chat-roundtrip probe
through one guarded safeReadTurnState() wrapper so a mid-poll readTurnState()
throw is handled consistently everywhere: it means "no reliable signal" ->
degraded widen + observable telemetry, never a silent false-red (the prior
readDegraded swallow) nor a spurious level-error (the prior unguarded
readBaseline/readTurnComplete escape). A genuinely-empty degraded turn still
reds at the ceiling.
Folds completing the PR's own items:
- item-1 first-send cap: guard the in-send press against SEND_PRESS_MIN_BUDGET_MS
so a near-hang type can't floor press to ~1ms and produce a generic
level-error; classify distinctly as send-budget-exhausted. Only press is
guarded (type opens the envelope), so a legitimately-small pageTimeoutMs still
issues a healthy first send.
- item-3 null-header: suppress the finally-block fallback probe.message.send once
a real-header boundary already fired, so a retry whose winning resend lands no
POST no longer emits a second null-header boundary (mis-attributed
edge_interference_signal).
Also add "abort" to the errorDesc JSDoc enumeration (zero-risk).
Red-green covered for all three behavioral items against the real
runLevel/readTurnComplete path with a faithful fake.
Tests for the #5882 bucket-(b) follow-up hardening:
- Budget-exhaustion retry guard (red-green): a near-exhausted-budget retry no
longer attempts a doomed ~1ms-floored resend (type invoked exactly once).
- Degraded-path floor (red-green): a degraded page (sseAttachFailed, no
completion signal) with a late-but-present token resolves GREEN instead of a
base-floor false-red; a genuinely-empty degraded run still reds.
- Retry telemetry re-attribution (focused test): a retry-rescued GREEN turn
records the winning attempt's edge headers on probe.message.send.
- Coverage: FIFO-cap CVDIAG_MAX_OUTSTANDING_STARTS_PER_URL eviction backstop;
DEBUG-auto-disarm fail-closed (disarmed => no raw-byte capture);
alternate-content / raw-byte block SKIPPED on the container-success path.
- Fake fix: makeLateTokenBrowser now mints per-page state so L3 and L4 each run
an independent stall+retry cycle (was a shared-page singleton that leaked
sendCount from L3 into L4, so the L4 retry path was never genuinely exercised).
Follow-up to #5882 (bucket-(b) CR items). Three behavioral/telemetry fixes
plus one documentation clarification, all in d4-chat-roundtrip.ts:
- Budget-exhaustion retry guard: skip a non-completion retry resend when the
remaining wall-clock budget is below RETRY_MIN_BUDGET_MS (750ms). A late
resend previously floored its type/press action timeout to ~1ms, throwing a
page-fault-shaped error that mis-classified the stall as a generic red — a
spurious-red flap source. The stall now reds on its own terms.
- Degraded-path floor: when the SSE interceptor silently no-ops
(sseAttachFailed), no completion signal ever arrives, so the poll could only
fall into the never-observed branch and pin the deadline to the base floor —
reintroducing the slow-first-token false-red #5882 targets. Consult
sseAttachFailed to WIDEN the never-observed wait to the per-attempt ceiling so
a late-but-present token on a degraded page is still captured.
- Retry edge-header re-attribution: on a retry-rescued turn, re-arm the
message-POST edge-header capture (messageSendEdge / lastMessagePostResp /
emitMessageSend latch) so probe.message.send / edge_interference_signal / the
DEBUG raw-byte sample reflect the WINNING attempt, not the stalled first one.
- Document why lastStoppedAtMs is retained on the d4 TurnState (write-only in
d4; part of the shared attachSseInterceptor global shape the d6 run-signal
snapshot also mirrors) so it does not read as dead code.
## Root cause
D4's L4 flap was a **stalled turn, not a client render race**: aimock
served content but the page never rendered it (the real 20:16:52Z
failure). The turn stream never delivered a first token to the DOM
within the fixed `textPollTimeoutMs`, so the poll read the container
empty → spurious "empty assistant response" red.
The **prior fix (`ceae0c2c9`) was inert in production**. It keyed the
turn-complete decision off the `onSseEvent` Node-side seam, which the
real launchers never wire (Playwright has no per-SSE-event signal).
`sseObserved` therefore stayed `false` on the real path and the whole
extension collapsed to the pre-fix base budget floor. Its red-green used
a fake page that invoked the seam synthetically, so the deadness was
never caught.
## The fix (3 parts, mirrors `d6-all-pills.ts`)
1. **Wire the real completion signal.** `wirePlaywrightPage.goto` now
calls `attachSseInterceptor(page)` before navigation (injectable for
tests), seeding the page-side `__hk_runsFinished` /
`__hk_copilotRunning` turn-lifecycle globals at `document_start`. A new
`readTurnState()` `E2ePage` seam reads them via `page.evaluate`; the
first-token poll keys off **that** — the same transport-level
`RUN_FINISHED` + DOM run-stop edge d6 trusts — not the dead `onSseEvent`
seam.
2. **Fast-fail genuinely-empty turns.** With a real turn-complete edge,
a turn that completes with empty assistant text reds in ~`completion +
FIRST_TOKEN_GRACE_MS` (bounded by `FIRST_TOKEN_FAST_FAIL_MS` ≈ 15s)
instead of burning the flat 60s. A completed-empty turn still reds (no
masking) and is never retried.
3. **Retry-on-non-completion.** A turn **observed in-flight** that never
signals completion within budget (stalled/dropped stream) retries once
before red. **Never-observed** (dead/no-turn) runs stop at the base
floor with no retry. Total wall-clock is bounded by `pageTimeoutMs`
(per-attempt budget split), so the retry can't blow past the ceiling.
## CR findings resolved
- `abortSignal.aborted` checked **inside** the poll loop (was only at
level entry).
- Body-scrape fallback keeps `fromAssistantContainer=false` **and** no
longer clears `cvdiagResponseEmpty` — so a fallback-salvaged red can no
longer emit `terminal_outcome=ok`.
- Red/green never gated on `cvdiag` (telemetry-only); the `onSseEvent`
seam is documented telemetry-only.
- No-turn fallback budget capped by the per-attempt ceiling (bounded by
`pageTimeoutMs`).
- Fallback `tail.length > 20` floor and `split("\n")[0]` truncation
**removed** (they false-red'd short/multiline valid answers).
- Corrected stale "not started" / dead-seam comments.
## Real-surface red-green (NO fake page)
Real chromium + real `attachSseInterceptor` + real D4 driver against a
local fixture serving an SSE `/api/copilotkit` with an injected
first-token/stream delay (aimock-style). `sseObserved-on-real-path =
(runsFinished>0 || attrPresent)` is read from the actual page-side
globals.
**RED — pre-fix (interceptor UNWIRED), late-token (token@3s):**
```
MODE: unwired scenario: late-token
L3(chat).state: red summary: "empty assistant response"
elapsed_ms: 1444
sseObserved-on-real-path: false <-- dead seam proven
```
**GREEN — post-fix (interceptor WIRED), same late-token:**
```
MODE: wired scenario: late-token
L3(chat).state: green summary: ""
elapsed_ms: 6333
LAST readTurnState on REAL path: {"runsFinished":0,"attrPresent":true,"sawRunningTrue":true,"runningNow":true}
sseObserved-on-real-path: true <-- interceptor fires on real path
```
**completed-empty (WIRED) — still RED, fast-fail:**
```
L3(chat).state: red summary: "empty assistant response"
elapsed_ms: 5340 (~2.5s/level, not the 60s ceiling)
LAST readTurnState on REAL path: {"runsFinished":1,"attrPresent":true,"sawRunningTrue":true,"runningNow":false}
sseObserved-on-real-path: true
```
**recoverable-stall (WIRED) — attempt 1 never completes → retry →
GREEN:**
```
L3(chat).state: green summary: ""
elapsed_ms: 13423 (attempt 1 polls to per-attempt ceiling observed=true/complete=false, then resends; attempt 2 renders)
sseObserved-on-real-path: true
```
## Checks
- `tsc --noEmit`: clean
- `tsc -p tsconfig.build.json` (build): clean
- vitest `d4-chat-roundtrip.test.ts`: 56/56 pass (suite rewritten to
exercise the real `readTurnState` path + retry/fast-fail;
late-token→green, completed-empty→red, recoverable-stall→green via
retry, permanent-stall→red)
- oxlint: 0 errors (pre-existing warnings only)
Kept as a **draft**; do not merge.
---
## Follow-up (b572697ab): attempt-scoped completion — the dominant a1
false-red
CR flagged that the poll's `sseDone = runsFinished >= 1` read the
page-GLOBAL monotonic counter. A **prior run on the page**
(auto-greeting / initial-mount run) leaves `runsFinished >= 1` and
`sawRunningTrue` already latched **before the user's turn starts**, so
the poll thought THIS turn had already completed, saw the still-empty
container, and spuriously **fast-failed RED**. It only passed the old
tests because the fake reset per send.
### Fixes in this commit
1. **Completion is now ATTEMPT-SCOPED.** A per-attempt BASELINE
(`runsFinished` + `runStartCount`) is captured at send time; the turn is
complete only on a **new edge past the baseline** (`runsFinished >
baseline`, or a new `runStartCount` DOM run-start), never the
page-global `>= 1`. A fresh baseline is taken before each retry resend,
so a stale prior edge can't defeat the retry.
`TurnState`/`readTurnState` now surface `runStartCount` +
`lastStoppedAtMs` (the sse-interceptor already latches them), and the
grace window is stamped from the **real finished edge** rather than the
poll's local clock (fixes the ~500ms-short grace).
2. **Retry resend bounded by remaining budget.** The retry `sendTurn()`
type/press timeouts are capped to `hardCeiling` (were flat
`pageTimeoutMs`, which let a stalled resend push poll-phase wall-clock
~2x past the ceiling).
3. **Attach-fault is observable.** A failed `attachSseInterceptor` now
emits an `onAttachFault` marker + sets `TurnState.sseAttachFailed`, so a
silent regression to the inert base-floor path is detectable.
Folded (cheap): `probe.message.send` fallback moved into `finally`
(fires on nav/send throw); external `ctx.abortSignal` aborts labeled
`"abort"` (not `driver-error`) in the aggregate; corrected
fast-fail-floor / hardCeiling / poll-deadline doc comments.
### Real-surface RED→GREEN (real Chromium + real `attachSseInterceptor`
+ local SSE fixture)
The fixture fires a PRIOR run on mount (bumps the page-global counters),
then the user turn renders a first token at 2800ms — PAST the 2s grace
window. Driven through the REAL `createE2eSmokeDriver` +
`wirePlaywrightPage` (L3/chat, the every-service level).
**PRE-FIX (`sseDone = runsFinished >= 1`):**
```
a1-priorrun L3.state: red "empty assistant response" elapsed 2141ms <-- FALSE-RED (the bug)
recoverable L3.state: red "empty assistant response" elapsed 2171ms <-- retry defeated by stale prior edge
wired-late L3.state: green elapsed 3136ms (no prior run → no false completion)
completed-empty L3.state: red "empty assistant response" elapsed 2156ms
```
**POST-FIX (attempt-scoped baseline):**
```
a1-priorrun L3.state: green elapsed 3285ms <-- fixed: prior run no longer false-reds
recoverable L3.state: green elapsed 5192ms <-- retry rescues (fresh baseline per resend)
wired-late L3.state: green elapsed 3150ms
completed-empty L3.state: red "empty assistant response" elapsed 2707ms <-- fast-fail (~completion+grace, not the 9s ceiling)
sseObserved-on-real-path: true (all) sseAttachFailed: false
```
### Checks
- `tsc --noEmit`: clean · `tsc -p tsconfig.build.json` (build): clean
- vitest `d4-chat-roundtrip.test.ts`: **63/63** (added: a1-regression
L3+L4, completed-empty-one-send, L3 grace/fast-fail/retry, attach-fault
telemetry)
- oxlint: **0 errors** (4 pre-existing warnings only)
Still a **draft**; do not merge.
---
## Follow-up (a): grace-window timestamp — Node-stamp completion instant
Four reviewers flagged the same line in `runAttempt`:
```ts
completeAtMs = stoppedAtMs > 0 ? stoppedAtMs : Date.now();
```
where `stoppedAtMs` came from the page-side
`readTurnState().lastStoppedAtMs`. Two defects in one line:
1. **Stale on SSE-only completion.** When the turn is detected complete
via `sseDone = runsFinished > baseline` but no fresh DOM stop-edge fires
for THIS turn, `lastStoppedAtMs` still holds a **stale prior-run** value
→ `graceEnd = staleStop + FIRST_TOKEN_GRACE_MS` lands in the past → the
~2s grace window collapses to the base floor → a late-but-present first
token **false-REDs**.
2. **Cross-clock skew.** `lastStoppedAtMs` is stamped on the
**browser-page** `Date.now()`, but `graceEnd`/deadline math runs on the
**Node** clock → page↔Node skew mis-sizes the window.
### Fix (single-point, simplifying)
Stamp `completeAtMs = Date.now()` (**Node**) at the first poll iteration
that observes THIS turn complete; `readTurnComplete` no longer threads
`stoppedAtMs`. Both clocks are now consistent; the stamp lags the true
finished edge by at most one 500ms poll interval, well inside
`FIRST_TOKEN_GRACE_MS`. Preserved: completed-empty still fast-reds;
base-floor and `hardCeiling` caps intact; no masking. `lastStoppedAtMs`
is retained on `TurnState` (still consumed by conversation-runner /
sse-interceptor / d6).
### Tightenings (net-negative source LOC: +54/−58 = **−4**)
- Extracted the attempt-0 baseline IIFE onto the existing `readBaseline`
helper (DRY; removes a drift hazard on the completion-scoping path).
- Fixed stale comments: fallback `emitMessageSend()` now runs in the
`finally`; eviction comment (rides the `onResponse` wiring, not "always
active"); `lastStoppedAtMs` doc.
### Red-green (mandatory)
**Unit — stale-`lastStoppedAtMs` grace collapse** (`SSE-only completion
with a STALE lastStoppedAtMs …`): prior finished run +
`sseOnlyStaleStop` + prior stop aged 5s + token 700ms after completion
(inside a healthy grace window).
```
RED (pre-fix stamping: completeAtMs = staleStop): expected 'red' to be 'green' → RED
GREEN (Node-stamp fix): 64/64 pass → GREEN
```
**Real-surface no-regression** (real chromium + `wirePlaywrightPage` →
real `attachSseInterceptor` + local SSE fixture,
`repro_d4_a1_realsurface.mts`):
```
a1-priorrun aggregate green · L3 green elapsed 3292ms (SSE-only-late, lastStoppedAtMs stale at completion)
completed-empty aggregate red · L3 "empty assistant response" elapsed 2618ms (fast-fail, not the 9s ceiling)
recoverable aggregate green · L3 green elapsed 5161ms
wired-late aggregate green · L3 green elapsed 3131ms
sseObserved-on-real-path: true (all) sseAttachFailed: false
```
### Checks (this commit)
- `tsc --noEmit`: clean · build (`tsc -p tsconfig.build.json`): clean
- vitest `d4-chat-roundtrip.test.ts`: **64/64**
- oxlint: **0 errors** (4 pre-existing warnings only)
- commit: `7bb47e64d`
Still a **draft**; do not merge.
---
## Follow-up (commit `5047f7ba9`): make the SSE-only-stale grace guard
actually bite
Three reviewers noted that the `sseOnlyStaleStop` guard above did not,
in fact, guard the Node-stamp fix. Root cause in the fake
(`makeLateTokenBrowser.readTurnState()`): the driver takes the
per-attempt baseline via `readTurnState()` **before** the first send,
and at that point `lastSendAtMs===0` made `elapsed` (~epoch ms) exceed
`completeAt`, spuriously flipping `complete=true` at the **baseline**
read. That inflated the baseline `runsFinished` to `prior+1`, so the
current turn's real finished edge never rose **past** the baseline and
the driver's `sseDone = runsFinished > baseline.runsFinished` could
never fire. The SSE-only-stale-grace path was therefore never entered —
the test passed only because the token rendered directly (path (a)), so
it did **not** guard the Node-stamp fix.
**Test-only fix:** gate the fake's `complete` on `started` (a turn has
actually been sent). The pre-send baseline read is now a **true**
baseline (`runsFinished = prior + priorSendsDone`, no current-turn
finish); the finished edge is a genuine THIS-turn transition the driver
observes via `sseDone`; and the SSE-only completion path with a stale
`lastStoppedAtMs` is genuinely exercised.
### RED-on-revert proof (the guard bites)
Temporarily reverted the production stamp back to the pre-fix form
(`completeAtMs = stoppedAtMs > 0 ? stoppedAtMs : Date.now()`,
re-threading `stoppedAtMs` through `readTurnComplete`), ran the reworked
test, then restored the Node-stamp:
```
RED (pre-fix revert: completeAtMs = stoppedAtMs):
× SSE-only completion with a STALE lastStoppedAtMs … → GREEN
AssertionError: expected 'red' to be 'green'
Test Files 1 failed (1)
GREEN (Node-stamp restored: completeAtMs = Date.now()):
✓ SSE-only completion with a STALE lastStoppedAtMs … → GREEN
Test Files 1 passed (1)
```
The production revert was **not committed** — final state has the
correct Node-stamp; the production diff in this commit is
**comment-only** (no logic change).
### Comment corrections (this commit)
- Completed-empty deadline comment: previously claimed "base floor
always respected"; corrected to state that
`Math.min(Math.max(baseBudgetEnd, graceEnd), fastFailEnd,
attemptCeiling)` intentionally clamps **below** the floor (fast-fail — a
completed-empty turn must red fast, not burn the base budget).
- `FIRST_TOKEN_FAST_FAIL_MS` doc: now spells out the full `Math.min`
term rather than only the `Math.max(base, grace)` cap.
### Checks (this commit)
- `tsc --noEmit`: clean · build (`tsc -p tsconfig.build.json`): clean
- vitest `d4-chat-roundtrip.test.ts`: **64/64**
- oxlint: **0 errors** (4 pre-existing warnings only)
- diff: driver (comments) + test file only; no `repro_*`/`baseline`
staged
- commit: `5047f7ba9`
Still a **draft**; do not merge.
The sseOnlyStaleStop guard in makeLateTokenBrowser was ineffective: the
driver reads the per-attempt baseline via readTurnState() BEFORE the
first send, and at that point lastSendAtMs===0 made elapsed (~epoch ms)
exceed completeAt, spuriously flipping complete=true at the baseline
read. That inflated the baseline runsFinished to prior+1, so the current
turn's real finished edge never rose PAST the baseline and the driver's
sseDone = runsFinished > baseline.runsFinished could never fire. The
SSE-only-stale-grace path was therefore never entered — the test passed
only because the token rendered directly, so it did NOT guard the
Node-stamp fix.
Gate the fake's complete on started (a turn has been sent) so the
pre-send baseline read is a TRUE baseline (runsFinished = prior +
priorSendsDone). The finished edge is now a genuine THIS-turn transition
the driver observes via sseDone, and the SSE-only completion path with a
stale lastStoppedAtMs is genuinely exercised.
Proof the guard now bites (temporary production revert, not committed):
- pre-fix stamp (completeAtMs = stoppedAtMs > 0 ? stoppedAtMs : now):
test FAILS, expected 'red' to be 'green' (grace collapses).
- restored Node-stamp (completeAtMs = Date.now()): test PASSES.
Also corrects two production comments (3 reviewers flagged): the
completed-empty deadline comment claimed the base floor is always
respected, but Math.min(..., fastFailEnd, attemptCeiling) intentionally
clamps below the floor (fast-fail); and the FIRST_TOKEN_FAST_FAIL_MS doc
now spells out the full Math.min term. Comment-only, no logic change.
On an SSE-only completion (turn detected complete via runsFinished>baseline
with no fresh DOM stop-edge for THIS turn), readTurnState().lastStoppedAtMs
still held a stale prior-run value, and it is stamped on the browser-page
clock while graceEnd/deadline math runs on the Node clock. Feeding it into
Node-clock arithmetic pushed graceEnd into the past and collapsed the
FIRST_TOKEN_GRACE_MS window to the base floor, false-REDing a late-but-present
first token.
Stamp completeAtMs from Date.now() (Node) at the first poll that observes
THIS turn complete; readTurnComplete no longer threads stoppedAtMs. Also DRY
the attempt-0 baseline onto the existing readBaseline helper and fix stale
comments (fallback emit is in finally; eviction rides the onResponse wiring;
lastStoppedAtMs doc). completed-empty still fast-reds; base floor and
hardCeiling caps preserved.
Adds a red-green unit test modelling an SSE-only completion with a stale
lastStoppedAtMs (grace collapses pre-fix, honored post-fix).
The first-token poll keyed turn-complete off the page-GLOBAL monotonic
`runsFinished >= 1` / latched `sawRunningTrue`. A PRIOR run on the page
(auto-greeting / initial-mount run) leaves those already satisfied when the
user's turn starts, so the poll treated THIS turn as already complete, saw the
still-empty container, and spuriously fast-failed RED — the a1 false-red.
Fix: capture a per-attempt BASELINE (`runsFinished` + `runStartCount`) at send
time and treat the turn complete only on a NEW edge past that baseline
(`runsFinished > baseline` / a new `runStartCount` DOM run-start). A fresh
baseline is taken before each retry resend, so a stale prior edge can no longer
defeat the retry. The grace window is now stamped from the REAL finished edge
(`lastStoppedAtMs`) rather than the poll's local clock (fixes the ~500ms-short
grace). `TurnState` / `readTurnState` are extended to surface `runStartCount`
and `lastStoppedAtMs` (the sse-interceptor already latches them).
Also:
- Bound the retry resend's type/press action timeouts by the remaining budget
to `hardCeiling` (was flat `pageTimeoutMs`, letting a stalled resend push
poll-phase wall-clock to ~2x past the ceiling).
- Surface an interceptor-attach fault (`wirePlaywrightPage.goto`) via an
injectable `onAttachFault` marker + `TurnState.sseAttachFailed` so a silent
regression to the inert base-floor path is detectable, not invisible.
- Move the `probe.message.send` fallback emit into the `finally` (idempotent)
so it fires on nav/send throw paths too.
- Label external `ctx.abortSignal` aborts as `"abort"` (not `"driver-error"`)
in the aggregate, matching the per-level classification.
- Correct the fast-fail-floor / hardCeiling / poll-deadline doc comments.
Tests: a1 regression (prior finished run + in-flight turn → not false-red) at
L3 and L4; completed-empty does exactly ONE send (retry does not fire); L3
coverage for the grace/fast-fail/retry path; attach-fault telemetry surfaces.
The prior D4 first-token fix (ceae0c2c9) was INERT in production: it keyed the
turn-complete decision off the `onSseEvent` Node-side seam, which the real
launchers never wire (Playwright has no per-SSE-event signal). `sseObserved`
therefore stayed false on the real path and the whole extension collapsed to the
pre-fix base budget floor. Its red-green used a fake page that invoked the seam
synthetically, so the deadness was never caught.
Root cause of the flap: a STALLED turn (RUN_FINISHED served by aimock but the
page never rendered it — the real 20:16:52Z failure), NOT a mere client render
race. So we need a real completion signal AND a retry for never-completed turns.
Three-part fix (mirrors d6-all-pills' production-wired signal):
1. Wire the real signal. `wirePlaywrightPage.goto` now calls
`attachSseInterceptor(page)` before navigation (injectable for tests), seeding
the page-side `__hk_runsFinished` / `__hk_copilotRunning` turn-lifecycle
globals at document_start. A new `readTurnState()` E2ePage seam reads them via
`page.evaluate`; the first-token poll keys off THAT — the same
transport-level + DOM run-stop edge d6 trusts — not the dead onSseEvent seam.
2. Fast-fail genuinely-empty turns. With a real turn-complete edge, a turn that
completes with empty assistant text reds in ~completion+grace (bounded by
FIRST_TOKEN_FAST_FAIL_MS ~15s) instead of burning the flat 60s. A
completed-empty turn still reds (no masking) and is never retried.
3. Retry-on-non-completion. A turn OBSERVED in-flight that never signals
completion within budget (stalled/dropped stream) retries once before red.
Never-observed (dead/no-turn) runs stop at the base floor, no retry. Total
wall-clock is bounded by pageTimeoutMs (per-attempt budget split).
CR findings resolved: abortSignal.aborted checked inside the poll loop; the
body-scrape fallback keeps fromAssistantContainer=false AND no longer clears
cvdiagResponseEmpty, so a fallback-salvaged red can't emit terminal_outcome=ok;
red/green never gated on cvdiag (telemetry-only); the no-turn budget is capped by
the per-attempt ceiling; the fallback `tail.length>20` floor and
`split("\n")[0]` truncation removed (false-red on short/multiline answers);
stale "not started" / dead-seam comments corrected.
Real-surface red-green (real chromium + real attachSseInterceptor + real driver
against a local fixture serving SSE /api/copilotkit with injected stream delay):
- RED (pre-fix, interceptor unwired): late-token turn -> red "empty assistant
response" in ~1.4s; sseObserved-on-real-path = FALSE (dead seam proven).
- GREEN (post-fix, interceptor wired): same late-token -> green;
readTurnState on real path = {attrPresent:true,sawRunningTrue:true,...};
sseObserved-on-real-path = TRUE.
- completed-empty (wired): still RED, fast-fail ~2.5s/level, runsFinished:1.
- recoverable-stall (wired): attempt 1 never completes -> retry -> green.
Unit suite (56 tests) rewritten to exercise the real readTurnState path plus the
retry/fast-fail behaviors; tsc + build + vitest all pass.
OSS-451 shipped because nothing linked a demo page's CopilotKit runtimeUrl
to the existence of the /api route it names. The only automatic pre-merge
gate for showcase/** is a Docker build, which compiles a page that
references a non-existent route just fine (runtimeUrl is an unchecked
string) — so the page-404-on-load class was invisible.
Add a static validator (validate-runtime-routes.ts) that, for every SHIPPED
demo (a demo listed in its integration's manifest `features`), asserts its
runtimeUrl resolves to a real route dir under src/app/api. Unshipped /
experimental demos (not in `features`) and not_supported_features are
skipped, so incomplete placeholders don't fail the gate — but promoting one
into `features` immediately starts enforcing it. A baseline file can
grandfather pre-existing violations; the fleet is currently clean (0).
Wire it into a new pre-merge workflow (showcase_validate-wiring.yml) that
runs on every showcase/integrations PR alongside the build check. Add it to
branch-protection required checks to make it blocking.
Regression test proves it flags the exact OSS-451 shape (shipped demo,
missing route) while passing existing/base routes and skipping unshipped.
Verified: npm run validate-routes -> clean fleet-wide; removing the 3
OSS-451 routes -> flags exactly those 3; full showcase/scripts vitest suite
(2151 tests) green.
Refs OSS-451
D4's L4 "tools" probe read the assistant-message container by polling
textContent for a fixed textPollTimeoutMs. On a run where the first token
rendered into the DOM slightly later than that budget — on a turn that
genuinely produced content — the poll exhausted and read the container as
empty, yielding a spurious "L4: empty assistant response" red (a client-side
first-token render race, not a real-LLM/fixture issue).
Harden the wait to key off the AG-UI SSE turn lifecycle rather than a fixed
timeout: track RUN_FINISHED/RUN_ERROR on the already-wired onSseEvent seam,
keep polling while a turn is in-flight (up to the pageTimeoutMs hard ceiling),
and after completion allow a small bounded first-token grace window for the
DOM to paint. A turn that completes with no content ever still fails, and when
no SSE stream is observed at all the poll falls back to the base budget floor
(unchanged pre-fix behavior, no hangs).
Adds red-green tests exercising the real runLevel wait path: a late-but-present
first token now passes; a genuinely-empty completed turn still fails.
Three wired demos 404'd on load: their pages point <CopilotKit runtimeUrl>
at /api/copilotkit-<demo>, but those route handlers were never created in the
Mastra integration (the pages were mirrored from langgraph-python without
porting the routes). The runtime-info fetch 404'd, so the page never mounted
(runtime_info_fetch_failed).
Add the three dedicated routes, mirroring the proven copilotkit-beautiful-chat
pattern. The two A2UI demos set a2ui.injectA2UITool:false (weatherAgent already
owns generate_a2ui — avoid a double-bind) and pin defaultCatalogId to the
catalog the page registers. agent-config registers the agent id the page
requests (agent-config-demo).
Page-load fix only; full behavioral parity (dedicated Mastra agents) is OSS-381.
Verified: next build compiles all three into the route manifest; POST returns
400 (route resolves) identically to copilotkit-beautiful-chat, vs 404 for a
nonexistent route.
Refs OSS-451
Add verify-shell-docs (+ unit tests), probe-shell-docs, probe-claude-quickstarts
(Playwright), and check-claude-quickstarts-runtime (extracts and runs the
documented commands/snippets) to gate the shell-docs build and quickstart
runtime. Includes CR hardening: drained server pipes, temp-dir cleanup,
SIGKILL escalation, a stack-trace-leak guard that matches SSE-escaped newlines,
and a fixed false-negative in the missing-import check.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Align the claude-sdk-python and claude-sdk-typescript demo integrations behind
the published quickstarts: move the @region markers used for doc snippet
extraction, add the state-streaming and weather-tool snippet files, and add
setup-doc content. Runtime alignment: the TS agent handlers consistently emit
text/event-stream; the streaming snippets emit a fresh STATE_SNAPSHOT per delta
and drop the undeclared partial-json dependency.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Turn on generated Shell docs for claude-sdk-python and claude-sdk-typescript:
quickstarts, framework registry data, docs links, and setup snippets.
Generalize the shared feature docs (state-streaming, HITL/interrupt,
tool-rendering, subagents, programmatic-control) to framework-neutral wording
so they read correctly across integrations. Includes review/audit fixes: the
valid claude-sonnet-4-6 model id, feature-card links pointing at pages that
exist, ms-agent-harness-dotnet docs-folder + tab-default routing, and concrete
state-streaming API names kept as neutral examples.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
## Summary
Renames the **Bots SDK → Channels SDK** (leadership decision, OSS-438).
**Names only — no behavior change.**
| old | new |
|---|---|
| `@copilotkit/bot` | `@copilotkit/channels` |
| `@copilotkit/bot-ui` | `@copilotkit/channels-ui` |
| `@copilotkit/bot-slack` | `@copilotkit/channels-slack` |
| `@copilotkit/bot-teams` | `@copilotkit/channels-teams` |
| `@copilotkit/bot-discord` | `@copilotkit/channels-discord` |
| `@copilotkit/bot-telegram` | `@copilotkit/channels-telegram` |
| `@copilotkit/bot-whatsapp` | `@copilotkit/channels-whatsapp` |
Package dirs renamed (`git mv`); versions carried over.
## What changed
- **Packages:** dir renames, `name` fields, `repository.directory`,
descriptions, and `workspace:` cross-deps rewired in lockstep.
`createBot` and other **API names are unchanged** (out of scope).
- **Release plumbing:** `release.config.json` scope keys +
`versionSource`, `ReleaseScope` union in
`scripts/release/lib/config.ts`, the
`canary`/`stable-release`/`publish-release` workflow scope dropdowns,
and `verify-release-scope-dropdowns.sh`.
- **Consumers:** `examples/slack` (Kite) + `examples/teams` — deps, the
load-bearing `jsxImportSource` pragma, build globs, imports.
- **Docs (`showcase/shell-docs`):** MDX package refs, content dirs
`docs/bots`→`docs/channels` and `reference/bot`→`reference/channels`,
nav registry, and **permanent redirects** from the old `/bots` and
`/reference/bot` URLs.
## Verification
- All 7 `@copilotkit/channels*` packages build; package test suites pass
(143+ in `channels`).
- `examples/slack` + `examples/teams` typecheck clean against the
renamed packages.
- `verify-release-scope-dropdowns.sh` green; release notification
wrapper test 30/30; `release:prepare` dry-runs for `channels` and
`channels-slack` resolve.
- `showcase/shell-docs` `frontend-options` test + full `tsc --noEmit`
clean.
- Zero stray `@copilotkit/bot` / `packages/bot` refs remain.
## ⚠️ Before merge / after merge
- **Do not squash-lose the deprecation step:** after these publish, run
`npm deprecate @copilotkit/bot@"*" "Renamed — install
@copilotkit/channels instead."` for the **5 published** old packages
(`bot`, `bot-ui`, `bot-slack`, `bot-teams`, `bot-discord`).
`bot-telegram`/`bot-whatsapp` were never published.
- New package names have **no npm version history**; the `package.json`
`version` seeds the first publish. Dry-run the OIDC publish for a
never-published scope first.
- Public-surface rename → stakeholder sign-off (kept as draft).
Refs OSS-438
🤖 Generated with [Claude Code](https://claude.com/claude-code)
The launch-site comment claimed process substitution leaves $! pointing at
the real node process. That is false and contradicts the file header: $!/
AGENT_PID is the wrapping subshell, and the real npm->node server is a
descendant reached only via the tree-kill (the reason _kill_agent_tree exists).
Rewritten in both langgraph-typescript and strands-typescript entrypoints so no
maintainer reintroduces a bare kill.
Also in langgraph-typescript: clarify that the SIZE_PID kill is a retained
belt-and-suspenders backstop to the $BASHPID PPID-walk (not dead code), and
re-anchor the startup-grace rationale to the real cause (the top-level
@langchain/langgraph-api import cost), since the prod path no longer uses
langgraph-cli dev.
Comment-only; no executable code changed.
The bounded re-scan loop's sleep 0.2 was unguarded. Under set -e on a base image
whose sleep can return non-zero (e.g. a future busybox/Alpine rebase), a failed
sleep would abort the tree-kill mid-walk — root never killed, real npm→node
server left orphaned. Add || true so the walk completes regardless of sleep's
exit status. No behavior change on the current Debian base (coreutils sleep
succeeds). Helper kept byte-identical across both entrypoints.
Both langgraph-typescript and strands-typescript entrypoints inferred which
process exited via a post-hoc kill -0 if/elif after wait -n. That inference is
racy: on a near-simultaneous exit both PIDs are dead by probe time, so the first
kill -0 branch always wins and mislabels the diagnostic (naming the agent when
Next.js actually exited, attaching the wrong code to the wrong name). Use
bash's wait -n -p REAPED_PID (bash >= 5.1; node:22-slim ships 5.2) to capture
the actual reaped PID and key the message off it. Exit code (incl. 137) and the
final exit $EXIT_CODE are preserved; || EXIT_CODE=$? guard is unchanged.
Three related size-watchdog hardening changes in the langgraph entrypoint:
- Trap-order leak window: the size sub-loop was backgrounded (`( … ) &`,
SIZE_PID=$!) BEFORE its reaping `trap … EXIT` was registered, so an outer
SIGTERM landing in that window exited the watchdog subshell with no trap
armed and orphaned the sub-loop (reparented to PID 1, spinning for the
container's life). Arm the reaping trap FIRST, and reap via a $BASHPID
PPID-walk that finds the child regardless of whether SIZE_PID is assigned
yet — no ordering-dependent leak.
- Startup-grace size coverage: the size monitor started only AFTER the
up-to-180s startup-grace loop, leaving the size ceiling unguarded during a
pathological cold boot. Start it BEFORE the grace loop; safe because
_watchdog_check_size_once already fail-closes on every not-yet-ready
condition (agent PID not alive, PERSIST_DIR missing, non-numeric size/
threshold), so early cycles are harmless no-ops until the dir grows.
- cleanup() comment accuracy: corrected the note claiming WATCHDOG_PID
"forks nothing that outlives it" — it DOES fork the size sub-loop; the
bare `kill $WATCHDOG_PID` is safe because the watchdog's own inner EXIT
trap reaps that child, not because it forks nothing.
Proven RED->GREEN on the real entrypoint in node:22-slim: pre-fix the size
sub-loop keeps ticking after the watchdog exits (orphan) and the size monitor
spawns after the grace loop (unguarded); post-fix the sub-loop is reaped (0
ticks) and the monitor runs during grace (size-check fires within the grace
window). --check-size-once seam re-verified under/over threshold.
The numeric-config validator accepted any positive integer (`[1-9][0-9]*`),
so a 20+ digit override overflowed bash's signed-64-bit arithmetic and either
wrapped to a negative/garbage magnitude or aborted the `[ -ge ]` test with
"value too great for base" — which, suppressed to false inside the guard's
`if`, silently disabled the guard for the container's lifetime (the exact
fail-open class this validator exists to prevent).
Add a 10-digit length cap (max 9,999,999,999 — comfortably inside int64,
far above any real interval/threshold/strike knob) checked BEFORE the digit
`case`, since an all-digit 23-char value would otherwise pass validation.
A too-long value now takes the same WARN + fall-back-to-default fail-safe
path as every other bad override. Byte-identical across both entrypoints.
Proven RED->GREEN on the real entrypoints in node:22-slim: pre-fix a 23-digit
value survives validation and `$(( x * 3 ))` yields int64-wrapped garbage;
post-fix it WARNs, clamps to the default, and arithmetic is correct.
The /proc PPID walk forked an awk process for every entry in the process
table on every scan pass. Replace the `echo "${stat##*) }" | awk '{print $2}'`
pipeline with the `read` builtin, which word-splits the post-comm remainder
("STATE PPID PGRP …") on IFS and captures the 2nd field with no subprocess.
Byte-identical across the langgraph-typescript and strands-typescript
entrypoints. Non-behavioral; bash -n + shellcheck --severity=warning clean.
Renames the Bots SDK to the Channels SDK. Names only — no behavior change.
- 8 packages @copilotkit/bot* -> @copilotkit/channels* (git mv dirs, names,
workspace: cross-deps). Now includes @copilotkit/bot-intelligence ->
@copilotkit/channels-intelligence (landed on main via #5761; unpublished, so
renamed fresh with the family).
- release.config.json scope keys + versionSource; ReleaseScope union;
canary/stable-release/publish-release scope dropdowns; verify script
- examples/slack (Kite) + examples/teams: deps, jsxImportSource, imports
- showcase/shell-docs: content dirs docs/bots->docs/channels and
reference/bot->reference/channels, nav registry, redirects
createBot and other API names unchanged. Old @copilotkit/bot* to be deprecated
after the new packages publish (bot-intelligence was never published).
Re-derived onto latest main (was conflicting after #5761 landed).
Refs OSS-438
## What & why
Two showcase agent containers (**langgraph-typescript**,
**strands-typescript**) could enter a *running-but-dead* state: Railway
showed the service `● Online` while `/api/health` returned **HTTP 502**.
This took down all 36 LGT dashboard cells on staging **and** prod
(prod's `.langgraph_api` had crossed the 200 MB size-watchdog
threshold).
**Root cause:** the agent is launched via process substitution (`... &>
>(awk …) &`), so `$AGENT_PID` (`=$!`) is the **wrapper subshell**, not
the real `npm`→`node` server that holds the port. Every watchdog/cleanup
did a bare `kill -9 $AGENT_PID`, which reaped only the subshell and
**orphaned the real server** (reparented to PID 1, still bound to the
port). The watchdog's "kill agent → container restart → boot-purge"
contract therefore never fired: the frontend kept proxying to a dead
agent → 502 forever.
## Fixes (each with local red-green on the real entrypoint in
`node:22-slim`)
1. **cleanup() EXIT trap** → routes through `_kill_agent_tree` (was
orphaning the agent on every SIGTERM/redeploy).
2. **`_kill_agent_tree`** → `/proc`-based tree-kill with a bounded
re-scan (root killed last) so mid-walk forks can't escape; refuses PID ≤
1 (fail-closed).
3. **size-watchdog** hardened against non-numeric `du` and transient
errors (no silent gate-disable, no permanent loop death).
4. **strands health-watchdog** → 180 s startup-grace window (parity with
langgraph); the now-effective kill would otherwise loop a slow cold
start.
5. **`wait -n` under `set -e`** → capture exit code so the restart
diagnostic isn't dead code on the primary (137) path.
6. **structural:** one `_require_int` validator over *every*
operator-overridable numeric knob (fail-safe to default), and **every**
wrapped-PID kill (incl. `NEXTJS_PID`) routed through the guarded
tree-kill; dangerous `${AGENT_PID:-0}` sentinel removed.
7. **`_require_int`** requires a positive integer (rejects `0` and
leading-zero/octal).
## Incident status
Staging **and** prod LGT were restored immediately via redeploy
(boot-purge cleared the oversized state) — both `/api/health` → 200.
This PR stops the recurrence.
## Review
Converged through a 5-round unbiased review-fix loop (1 + 4
confirmation), zero mandatory findings at close, all load-bearing guards
independently re-verified. `bash -n` + shellcheck (`-S warning`) clean;
170/170 shell bats pass.
## Follow-up (tracked, separate PR — non-load-bearing)
`_require_int` upper-bound clamp (LOW arith-overflow, needs a 20+-digit
value); a stale `cleanup()` comment; size-guard unarmed during the
startup-grace window; SIZE_PID trap-registration micro-window;
diagnostic label on near-simultaneous exit; cosmetic log nits; startup
readiness `sleep 3`+`kill -0` probes the wrapper subshell; no dedicated
Next.js frontend watchdog.
## Summary
Lets the `@copilotkit/bot` SDK run from **Intelligence-delivered
events** without a second programming model, and adds the runtime `bots`
declaration API. A managed event (delivered by Intelligence) runs the
*same* customer handlers, tools, context, commands, Bot UI, and agents
as local/custom adapters — the managed path is "just another
`PlatformAdapter`," fed by injected transports.
This is the **OSS / SDK slice** of the Hosted Managed Bots work. The
credentialed transports (Realtime Gateway, Connector Outbox) and the
frozen shared contracts live elsewhere (see *Out of scope*); this PR
ships the seams they plug into, fully runnable headless.
Relates to **OSS-360** (runtime bots API), **OSS-361** (run the SDK from
Intelligence events), **OSS-363** (Slack render/codec reuse).
## What's in here
- **`intelligenceAdapter()` bridge** (`@internal`, not publicly
documented) — implements `PlatformAdapter` over two injected transports:
`DeliverySource` (inbound) + `EgressSink` (outbound). Ingress →
`onTurn`/`onCommand`/`onInteraction`/`onThreadStarted`/`onReaction`; ack
on success / nack on throw (at-least-once). Egress emits generic
operations carrying `BotNode[]` IR with **deterministic ids**
(`turnId:seq`, reset per turn) so a redelivered turn reproduces the same
ids for the Connector Outbox to dedupe. Idempotency lives at egress, so
the managed path skips ingress dedup (`skipIngressDedup`) — a redelivery
re-runs rather than being dropped.
- **Runtime `bots` API** — `new CopilotRuntime({ intelligence, bots })`,
accepted by TypeScript **only when `intelligence` is configured**
(discriminated union). `createBot({ name })`; `startManagedBots()`
validates names (required, identifier-style, unique — fail-loud), builds
activation metadata, and wires each bot to its resolved transport.
- **`PlatformCodec` seam** + Slack egress codec (`slackCodec`) composing
the existing pure `renderSlackMessage`, so IR→native rendering is shared
(no Bolt/creds) instead of duplicated.
- **Backwards-compatible SDK foundations**: `bot.addAdapter()` +
optional `adapters`, deferred backend resolution at `start()` with
`stateStore`-provider precedence (+ multi-provider warning),
`bot.transcripts` throws pre-start, optional
`eventId`/`turnId`/`deliveryId` on ingress + handler context. Existing
`createBot` callers and every `PlatformAdapter` implementer are
unaffected.
- **In-memory transports + fixture tests** — the full dispatch path
(envelope in → handler runs → egress op out) runs with zero
Slack/Intelligence/network.
## Out of scope (external / separate tickets)
- **Realtime Gateway + Connector Outbox transports** — implemented in
the closed-source repo against the `DeliverySource`/`EgressSink`
interfaces shipped here.
- **Shared contracts freeze (OSS-377)** — consumed here via a minimal,
isolated placeholder (`managed/contracts.ts`, marked `TODO(OSS-377)`);
swaps in via one import change.
- **OSS-363 ingress normalization** — the egress codec is done;
extracting the pure Slack event→neutral mapping out of the Bolt listener
(so local + Intelligence ingress share it) is the remaining, higher-risk
half and is left to that ticket (`TODO(OSS-363)`).
## Testing
TDD throughout (RED→GREEN per behavior). New: managed adapter
dispatch/ack-nack/ids/run-renderer/exclusivity, all-kinds routing, name
validation + metadata + lifecycle, runtime `bots` option, Slack codec.
Full suites green: `bot` 147, `bot-slack` 256, `runtime` 1574. All
builds typecheck (`bot`/`bot-slack`/`bot-discord`/`runtime`);
oxlint/oxfmt clean.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
The _require_int validator in the langgraph-typescript and strands-typescript
entrypoints accepted '0' and leading-zero/octal forms like '010'/'08'. Operator
typos on any numeric knob then broke a guard:
- SIZE_THRESHOLD_MB=0 kills the agent on cycle 1 (instant restart loop)
- HEALTH_STRIKE_LIMIT=0 kills on first probe miss
- SIZE_CHECK_INTERVAL=0 / HEALTH_CHECK_INTERVAL=0 busy-spin on 'while sleep 0'
- '010' is read as OCTAL (8) in arithmetic; '08'/'09' abort under set -e
Tighten the predicate to accept only a positive integer with no leading zero
([1-9][0-9]*). Invalid values keep the existing fail-safe behavior: WARN and
fall back to the documented default. Helper stays byte-identical across both
files.
CLASS 1 (guard silently disabled by a bad numeric override): add a reusable
_require_int validator and run it at startup over EVERY operator-overridable
numeric knob in both entrypoints (size threshold/interval, startup grace,
health-probe interval, strike limit). A non-integer/empty override now WARNs
and falls back to the documented default instead of breaking a sleep/loop/
arithmetic test. Closes instance #3 (LANGGRAPH_SIZE_CHECK_INTERVAL='60s'
killing the size-monitor loop on its first iteration).
CLASS 2 (wrapped-PID orphan + kill-0 footgun): route the cleanup() NEXTJS_PID
kill through _kill_agent_tree (it is process-sub-wrapped like the agent, so a
bare kill orphaned the real Next.js node server holding $PORT across redeploy).
Harden _kill_agent_tree and _agent_descendants to refuse a PID that is empty,
non-numeric, 0, or 1 (fail closed), making kill -9 0 / kill -9 1 structurally
impossible. Remove the ${AGENT_PID:-0} sentinel in the --check-size-once seam;
skip with a warning when AGENT_PID is unset instead of defaulting to 0.
Shared helper code kept byte-identical between the two entrypoints.
## Summary
- Backport the website messaging from CopilotKit/website#398 into the
shell-docs Slack and Microsoft Teams frontend pages.
- Replace the stale waitlist/managed-only framing with "get early
access" copy that presents CopilotKit Enterprise Intelligence as the
self-hosted or cloud-hosted production layer around the open source Bot
SDK.
- Frame Slack and Teams as frontends for agents built on any harness or
framework, while reserving production-layer terminology for CopilotKit
Enterprise Intelligence.
- Address browser review annotations on both pages: remove filler in the
opener, avoid setup-heavy lead copy, use "open source" without a hyphen,
add the full CopilotKit Enterprise Intelligence name to the CTA titles,
and keep CTA telemetry surfaces intact.
- Update the shell-docs nav test expectation so it matches the current
root IA, where Threads lives under Build Chat UIs rather than the
generated Intelligence Platform section.
## Validation
- `npm run lint` from `showcase/shell-docs` (passes with existing
warnings)
- `npm run typecheck` from `showcase/shell-docs`
- `npm run test` from `showcase/shell-docs`
- `npm run build` from `showcase/shell-docs`
## Notes
- Hydrated Git LFS assets locally with `git lfs pull` so the shell-docs
public asset tests could read real PNG bytes.
The package entry (@copilotkit/bot) re-exported runStateStoreConformance from
./testing/state-store-conformance, which imports vitest at module top-level.
An ESM re-export eagerly evaluates that module, so a bare
`import { createBot } from "@copilotkit/bot"` dragged vitest into every
consumer's runtime graph and threw ERR_MODULE_NOT_FOUND when vitest wasn't
installed (i.e. any production consumer).
- Drop the re-export from src/index.ts (entry is now vitest-free)
- Publish the conformance helper under the ./testing export subpath
- Declare vitest as an optional peerDependency (documents the /testing need)
- Update docs to import from @copilotkit/bot/testing
Names/behavior of the runtime API are unchanged; only the import path for the
test-only conformance helper moves.
Both entrypoints run under set -e. The tail `wait -n $AGENT_PID $NEXTJS_PID`
returns non-zero on the PRIMARY designed exit path (137 = size-gate/watchdog
SIGKILL of the agent tree, or an agent crash), so set -e aborted the script AT
that line — making EXIT_CODE=$?, the entire 'which process exited with code N'
diagnostic, and the final `exit $EXIT_CODE` dead code on exactly the
interesting exits. Capture the code with `EXIT_CODE=0; wait -n ... || EXIT_CODE=$?`
so the diagnostic and explicit exit run and preserve the exact code (incl. 137);
the container-restart path is unchanged.
Same class: langgraph's LANGGRAPH_SIZE_THRESHOLD_MB was used in
`[ "$DIR_SIZE_MB" -ge "$threshold" ]` with no numericity guard, so a
non-integer operator override made the test error and silently no-op the size
gate every cycle. Validate the threshold the same way DIR_SIZE_MB already is
(numeric case guard + 'size guard inactive' WARNING, then skip safely).