Skip to content

feat(kap-server): converge v3 WS protocol on client JSON ping/response - #3810

Merged
sailist merged 3 commits into
MoonshotAI:dev-v2from
sailist:feat-221-09-15-kap-v3-protocol-convergence
Sep 15, 2026
Merged

sailist merged 3 commits into
MoonshotAI:dev-v2from
sailist:feat-221-09-15-kap-v3-protocol-convergence

Conversation

@sailist

@sailist sailist commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

Related Issue

Internal protocol-convergence task (no tracking issue). The v3 WS protocol is still pre-stable (/api/v3/ws), so no compatibility is promised to older clients.

Problem

The v3 WebSocket protocol carried three redundant mechanism families that the protocol design review decided to remove:

  1. Handshake/heartbeat: the server pushed a hello frame on connect and relied on RFC 6455 control-frame ping/pong for liveness. Control-frame heartbeats only let the server detect dead clients, and browser WebSocket APIs can neither send ping frames nor observe pongs — so the client (the side that actually reconnects) had no working liveness signal.
  2. Request ids: client requests used per-message numeric id + ack replies, a second id concept disjoint from the REST string request_id.
  3. Timestamps: every message carried a numeric epoch-ms timestamp alongside ISO 8601 fields, two time formats for the same purpose.

What changed

Converge the v3 protocol on the reviewed design (ping/response + string request_id + ISO 8601 event_created_at):

  • Protocol schemas: delete hello/ack messages; add ping ({type:'ping', request_id}) and response ({type:'response', request_id, code, msg?}); subscribe/unsubscribe switch from numeric id to string request_id (z.string().min(1)); client message union now accepts ping.
  • Transport: wsConnectionV3 sends no handshake frame (protocol version is determined by the /api/v3/ws path) and answers ping immediately with response{code:0}; the WS control-frame heartbeat timer is removed from both the v3 and debug channels; subscribe/unsubscribe replies become response with the original timing guarantee (response precedes recovery messages); has_client_hello, V3_PROTOCOL_VERSION, V3_CAPABILITIES, and the hello-only serverId option are removed.
  • Timestamps: epochMsSchema and the numeric timestamp field are gone from all message bases; every event message carries event_created_at (ISO 8601). Value sources are unchanged (projection push time, wire event.time, cold-fold recordAtMs) — only the field name and format change. The duplicate isoDateTimeSchema in messages/base.ts now re-exports the agent-core-v2 one.
  • kimi-inspect: subscribes directly on connect (no hello wait) with crypto.randomUUID() request ids, matches response instead of ack, and compares event times as ISO strings.
  • Tests/docs: wsV3/protocolMessages/e2e suites updated (net test count 225 → 223, no increase); has_client_hello removed from the connections route docs.

Full suite on Linux: 873 files / 15,680 tests green.

Compatibility note: older clients that wait for hello (including the pre-built web bundle under apps/kimi-code/dist-web) will not connect until the app repo adapts to the new protocol — accepted, since v3 is pre-stable.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue (external PRs: the issue must have a maintainer's /approve).
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update.

@changeset-bot

changeset-bot Bot commented Sep 15, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 7969b2e

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Sep 15, 2026

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@7969b2e
npx https://pkg.pr.new/@moonshot-ai/kimi-code@7969b2e

commit: 7969b2e

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aaa69fbe05

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines 164 to +167
ws.addEventListener('open', () => {
this.reconnectAttempt = 0;
this.subscribeRequestId = crypto.randomUUID();
this.send({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Start client-side pings after opening the socket

When the TCP path is silently black-holed, this client never discovers the failure: the commit removes the server's control-frame heartbeat, but ChatWs only sends this subscription and never emits the new JSON ping or times out a missing response; GlobalEventsWs likewise sends nothing. Consequently neither close handler runs, so transcript updates and activity badges remain permanently stale instead of reconnecting and catching up. Add periodic ping/response tracking and force a reconnect on timeout in both shipped clients.

AGENTS.md reference: apps/kimi-inspect/AGENTS.md:L40-L40

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 8dfc3dc: both v3 clients (transcript ChatWs and activity GlobalEventsWs) now run a shared WsHeartbeat — a uuid-request_id JSON ping every 10s, at most one outstanding, reconnect after 2 missed intervals; timer gaps from throttled tabs/system sleep are forgiven instead of counted. Response dispatch consults heartbeat.consume() before the subscribe matcher.

@chatgpt-codex-connector

Copy link
Copy Markdown

💡 Codex Review

if (frame.type === 'ping') {
this.enqueue({ type: 'response', request_id: frame.request_id, code: ErrorCode.SUCCESS });

P2 Badge Retain server-side liveness detection

When a /api/v3/ws TCP path is silently black-holed, the new client heartbeat opens a replacement connection, but the old server socket never receives its close frame. This handler only answers client pings; after removing the server ping/pong timer, the old connection remains registered and subscribed—potentially for the OS TCP timeout—so repeated outages accumulate zombie connections and duplicate fanout. Keep an independent server-side ping/pong timeout alongside the application-level client heartbeat.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@sailist
sailist force-pushed the feat-221-09-15-kap-v3-protocol-convergence branch from 8dfc3dc to fe4ca35 Compare September 15, 2026 12:12

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fe4ca3568d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +166 to +167
if (frame.type === 'ping') {
this.enqueue({ type: 'response', request_id: frame.request_id, code: ErrorCode.SUCCESS });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retain server-side liveness probing

When a client disappears without completing the TCP close handshake—for example, a laptop loses its network while subscribed—the new JSON heartbeat only lets the client detect that the server is unreachable; the server never receives the ping or the client's eventual close. Since the control-frame heartbeat was removed from both wsConnectionV3.ts and wsConnectionDebug.ts, these sockets can remain in the connection registry, hub, session lanes, and debug collector indefinitely while continuing to receive fanout. Keep a server-originated ping/pong timeout (independently of the client JSON heartbeat) so abandoned connections are released.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 57c9772, without reintroducing a server-originated heartbeat: the protocol already requires clients to run their own JSON ping (kimi-inspect does so every 10s), so any live connection has constant inbound traffic. Both wsConnectionV3 and wsConnectionDebug now stamp lastInboundAt on every inbound frame and terminate the socket after 10 minutes of silence (idleTimeoutMs, checked every 60s), which releases abandoned connections from the registry, hub, session lanes, and debug collector.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 57c9772212

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +117 to +118
if (Date.now() - this.lastInboundAt <= this.idleTimeoutMs) return;
this.close();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep receive-only debug streams alive

For /api/v1/debug/ws, a healthy subscribed client normally sends only the initial subscribe frame while the server streams inspection envelopes, so outbound activity never refreshes lastInboundAt; this new check therefore closes every valid long-running debug stream roughly 11 minutes after subscription. Fresh evidence beyond the earlier heartbeat comment is this revision's inbound-idle replacement, which now deterministically disconnects healthy receive-only clients; retain ping/pong probing for this endpoint or add an explicit client heartbeat protocol before enforcing inbound idleness.

Useful? React with 👍 / 👎.

Replace the server-pushed hello handshake and ack replies with a unified
response message, move heartbeat from WS control frames to a
client-initiated JSON ping, switch client request ids from numbers to
string request_id, and replace numeric epoch-ms timestamps with ISO 8601
event_created_at across all v3 messages. Remove the WS-level heartbeat
from the debug channel as well, and migrate kimi-inspect to the new
handshake-free flow.
…clients

The server no longer probes liveness after the protocol convergence, so
both kimi-inspect clients (transcript and activity) now ping on an
interval with a uuid request_id, track the matching response, and drop
the socket into the reconnect path after two missed intervals. Timer
gaps from throttled tabs or system sleep are forgiven instead of being
counted as failures.
Since the protocol requires clients to run their own JSON ping, a
healthy connection always has inbound traffic. Track the last inbound
frame time on both the v3 and debug WS connections and terminate the
socket once it has been silent for idleTimeoutMs (default 10 minutes),
so abandoned connections are released from the registry, hub, and
session lanes without reintroducing a server-originated heartbeat.
@sailist
sailist force-pushed the feat-221-09-15-kap-v3-protocol-convergence branch from 57c9772 to 7969b2e Compare September 15, 2026 14:13

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7969b2e4ff

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +180 to +181
if (frame.type === 'ping') {
this.enqueue({ type: 'response', request_id: frame.request_id, code: ErrorCode.SUCCESS });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Send heartbeat responses ahead of queued data

When a live client has crossed the 1 MiB high-water mark, enqueue places this heartbeat response behind the existing application-message backlog. If that backlog takes more than two 10-second heartbeat intervals to drain, both shipped clients interpret the delayed response as a dead connection and reconnect even while they are still receiving server traffic; large recovery payloads or tool output on a slower link can therefore cause a reconnect loop. Send ping responses immediately or prioritize them over queued data so liveness is not coupled to application backpressure.

Useful? React with 👍 / 👎.

@sailist
sailist merged commit 7dfccba into MoonshotAI:dev-v2 Sep 15, 2026
13 checks passed
sailist added a commit that referenced this pull request Sep 16, 2026
#3810)

* feat(kap-server): converge v3 WS protocol on client JSON ping/response

Replace the server-pushed hello handshake and ack replies with a unified
response message, move heartbeat from WS control frames to a
client-initiated JSON ping, switch client request ids from numbers to
string request_id, and replace numeric epoch-ms timestamps with ISO 8601
event_created_at across all v3 messages. Remove the WS-level heartbeat
from the debug channel as well, and migrate kimi-inspect to the new
handshake-free flow.

* feat(kimi-inspect): add client-side ping/response heartbeat to v3 WS clients

The server no longer probes liveness after the protocol convergence, so
both kimi-inspect clients (transcript and activity) now ping on an
interval with a uuid request_id, track the matching response, and drop
the socket into the reconnect path after two missed intervals. Timer
gaps from throttled tabs or system sleep are forgiven instead of being
counted as failures.

* feat(kap-server): terminate inbound-idle WS connections after 10 minutes

Since the protocol requires clients to run their own JSON ping, a
healthy connection always has inbound traffic. Track the last inbound
frame time on both the v3 and debug WS connections and terminate the
socket once it has been silent for idleTimeoutMs (default 10 minutes),
so abandoned connections are released from the registry, hub, and
session lanes without reintroducing a server-originated heartbeat.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant