Skip to content

Commit e3fdd97

Browse files
author
SqlRush
committed
Surface remote websocket close codes
1 parent 1c9e530 commit e3fdd97

8 files changed

Lines changed: 19 additions & 5 deletions

File tree

cmd/claude/main.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -888,6 +888,7 @@ func runDaemonRemoteStream(ctx context.Context, runner conversation.Runner, now
888888
pumpState.FrameCount = result.FrameCount
889889
pumpState.ConnectCount = result.ConnectCount
890890
pumpState.ReconnectCount = result.ReconnectCount
891+
pumpState.CloseCode = result.CloseCode
891892
pumpState.LastPollAt = time.Now().UTC().Format(time.RFC3339Nano)
892893
pumpState.StreamEndedAt = pumpState.LastPollAt
893894
pumpState.StreamStopReason = daemonRemoteStreamStopReason(ctx, streamOptions, result)
@@ -906,6 +907,7 @@ func runDaemonRemoteStream(ctx context.Context, runner conversation.Runner, now
906907
structured["frame_count"] = pumpState.FrameCount
907908
structured["connect_count"] = pumpState.ConnectCount
908909
structured["reconnect_count"] = pumpState.ReconnectCount
910+
structured["close_code"] = pumpState.CloseCode
909911
structured["stream_started_at"] = pumpState.StreamStartedAt
910912
structured["stream_ended_at"] = pumpState.StreamEndedAt
911913
structured["stream_stop_reason"] = pumpState.StreamStopReason
@@ -1075,6 +1077,7 @@ func runDaemonRemotePoll(ctx context.Context, runner conversation.Runner, now ti
10751077
LastCursor: previous.LastCursor,
10761078
LastPollAt: now.UTC().Format(time.RFC3339Nano),
10771079
StatusCode: remoteFetch.StatusCode,
1080+
CloseCode: remoteFetch.CloseCode,
10781081
FrameCount: remoteFetch.FrameCount,
10791082
ConnectCount: remoteFetch.ConnectCount,
10801083
ReconnectCount: remoteFetch.ReconnectCount,
@@ -1096,6 +1099,7 @@ func runDaemonRemotePoll(ctx context.Context, runner conversation.Runner, now ti
10961099
structured["poll_url"] = pumpState.PollURL
10971100
structured["websocket_url"] = pumpState.WebSocketURL
10981101
structured["status_code"] = remoteFetch.StatusCode
1102+
structured["close_code"] = remoteFetch.CloseCode
10991103
structured["frame_count"] = remoteFetch.FrameCount
11001104
structured["connect_count"] = remoteFetch.ConnectCount
11011105
structured["reconnect_count"] = remoteFetch.ReconnectCount
@@ -1122,6 +1126,7 @@ func runDaemonRemotePoll(ctx context.Context, runner conversation.Runner, now ti
11221126
structured["websocket_url"] = pumpState.WebSocketURL
11231127
structured["last_cursor"] = pumpState.LastCursor
11241128
structured["status_code"] = pumpState.StatusCode
1129+
structured["close_code"] = pumpState.CloseCode
11251130
structured["frame_count"] = pumpState.FrameCount
11261131
structured["connect_count"] = pumpState.ConnectCount
11271132
structured["reconnect_count"] = pumpState.ReconnectCount
@@ -1143,6 +1148,7 @@ type daemonRemoteFetch struct {
11431148
Transport string
11441149
StatusCode int
11451150
NextCursor string
1151+
CloseCode int
11461152
FrameCount int
11471153
ConnectCount int
11481154
ReconnectCount int
@@ -1170,6 +1176,7 @@ func fetchDaemonRemoteEvents(ctx context.Context, registration remotepkg.Registr
11701176
FrameCount: ws.FrameCount,
11711177
ConnectCount: ws.ConnectCount,
11721178
ReconnectCount: ws.ReconnectCount,
1179+
CloseCode: ws.CloseCode,
11731180
Events: ws.Events,
11741181
Error: ws.Error,
11751182
}

cmd/claude/main_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,7 @@ func TestRunDaemonRemotePollPrefersWebSocket(t *testing.T) {
429429
}},
430430
}
431431
result := runDaemonRemotePoll(context.Background(), runner, time.Unix(200, 0).UTC())
432-
if result.StructuredContent["runtime_state"] != remotepkg.PumpRunning || result.StructuredContent["transport"] != "websocket" || result.StructuredContent["frame_count"] != 1 || result.StructuredContent["connect_count"] != 1 || result.StructuredContent["delivered_count"] != 1 || result.StructuredContent["error_count"] != 0 {
432+
if result.StructuredContent["runtime_state"] != remotepkg.PumpRunning || result.StructuredContent["transport"] != "websocket" || result.StructuredContent["frame_count"] != 1 || result.StructuredContent["connect_count"] != 1 || result.StructuredContent["close_code"] != 1000 || result.StructuredContent["delivered_count"] != 1 || result.StructuredContent["error_count"] != 0 {
433433
t.Fatalf("websocket poll = %#v", result.StructuredContent)
434434
}
435435
if len(auths) != 1 || auths[0] != "Bearer ws-token" {
@@ -439,7 +439,7 @@ func TestRunDaemonRemotePollPrefersWebSocket(t *testing.T) {
439439
if err != nil {
440440
t.Fatal(err)
441441
}
442-
if pump.Transport != "websocket" || pump.FrameCount != 1 || pump.ConnectCount != 1 || pump.DeliveredCount != 1 || strings.Contains(pump.WebSocketURL, "token=secret") {
442+
if pump.Transport != "websocket" || pump.FrameCount != 1 || pump.ConnectCount != 1 || pump.CloseCode != 1000 || pump.DeliveredCount != 1 || strings.Contains(pump.WebSocketURL, "token=secret") {
443443
t.Fatalf("pump = %#v", pump)
444444
}
445445
resume, err := manager.ResumeContext("agent/remote-lead", 3)

docs/cc-100-roadmap.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ M10 补充:remote settings 新增 `registrationUrl`/`authToken`,`advanced.br
115115

116116
M10 补充:新增 session-scoped `remote-pump.json` 和 daemon remote 消息泵;daemon tick 会在 ScheduleCron due tick 后优先读取 registered `websocket_url`,通过 Bearer auth 建立 WebSocket、读取单帧事件并复用 poll 解码/`RemoteTrigger` 注入路径;无 WebSocket 或 WebSocket 失败且存在 poll URL 时回退到带 cursor 的 poll 拉取。pump 状态会记录 transport、websocket/poll URL 脱敏值、cursor、HTTP status、event/delivered/duplicate/error 计数,`/status show remote` 可审计当前传输。完整 CCR WebSocket 常驻持久 stream 和云端协议 hardening 仍未完成。
117117

118-
M10 补充:remote WebSocket pump 现在支持单次 tick 内读取多帧事件,并在握手/读帧失败或非正常 close 时按可配置 backoff 重连;daemon 默认读取最多 8 帧、最多重连 2 次,并把 frame/connect/reconnect 计数写入 `remote-pump.json``/status show remote`。完整 CCR 云端 WebSocket 常驻持久 stream 与更深协议 hardening 仍未完成。
118+
M10 补充:remote WebSocket pump 现在支持单次 tick 内读取多帧事件,并在握手/读帧失败或非正常 close 时按可配置 backoff 重连;daemon 默认读取最多 8 帧、最多重连 2 次,并把 frame/connect/reconnect 计数和 WebSocket close code 写入 `remote-pump.json``/status show remote`。完整 CCR 云端 WebSocket 常驻持久 stream 与更深协议 hardening 仍未完成。
119119

120120
M10 补充:`internal/remote` 新增 callback 型 `StreamWebSocketEvents` primitive,可保持 WebSocket 连接逐帧解码并把事件批次交给调用方,支持 context 取消、可选帧上限、handler 错误传播、异常 close/读错后的 backoff 重连以及 `ReconnectAttempts < 0` 无限重连语义;该能力为 daemon 常驻 stream 托管接线打底。完整云端协议 hardening 仍未完成。
121121

docs/claude-code-go-rewrite-plan.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -746,7 +746,7 @@ test/parity/ # golden tests against TS/official behavior
746746
- 本轮补充:bridge direct server 新增 `GET /remote-service` discovery endpoint 和 WebSocket `remote_status` action,返回同一份 session-scoped remote service manifest;bridge manifest、direct `/manifest` 响应和 `/status show bridge` 同步暴露 `remote_service` capability,远端控制端可通过 HTTP 或已鉴权 WebSocket 查询 bridge/daemon 服务状态。完整 CCR 云端注册和消息泵仍未完成。
747747
- 本轮补充:remote settings 新增 `registrationUrl`/`authToken``advanced.bridge=true` 写出 remote service manifest 后会把 manifest POST 到注册 URL,并将 registered/failed/disabled、HTTP status、远端 session/websocket/poll 信息写入 session-scoped `remote-registration.json``/status show remote` 可审计注册状态且不泄露 token/query。完整 CCR 云端长连接仍未完成。
748748
- 本轮补充:新增 session-scoped `remote-pump.json` 和 daemon remote 消息泵;daemon tick 会在 ScheduleCron due tick 后优先读取 registered `websocket_url`,通过 Bearer auth 建立 WebSocket、读取单帧事件并复用 poll 解码/`RemoteTrigger` 注入路径;无 WebSocket 或 WebSocket 失败且存在 poll URL 时回退到带 cursor 的 poll 拉取。pump 状态会记录 transport、websocket/poll URL 脱敏值、cursor、HTTP status、event/delivered/duplicate/error 计数,`/status show remote` 可审计当前传输。完整 CCR WebSocket 常驻持久 stream 和云端协议 hardening 仍未完成。
749-
- 本轮补充:remote WebSocket pump 现在支持单次 tick 内读取多帧事件,并在握手/读帧失败或非正常 close 时按可配置 backoff 重连;daemon 默认读取最多 8 帧、最多重连 2 次,并把 frame/connect/reconnect 计数写入 `remote-pump.json``/status show remote`。完整 CCR 云端 WebSocket 常驻持久 stream 与更深协议 hardening 仍未完成。
749+
- 本轮补充:remote WebSocket pump 现在支持单次 tick 内读取多帧事件,并在握手/读帧失败或非正常 close 时按可配置 backoff 重连;daemon 默认读取最多 8 帧、最多重连 2 次,并把 frame/connect/reconnect 计数和 WebSocket close code 写入 `remote-pump.json``/status show remote`。完整 CCR 云端 WebSocket 常驻持久 stream 与更深协议 hardening 仍未完成。
750750
- 本轮补充:`internal/remote` 新增 callback 型 `StreamWebSocketEvents` primitive,可保持 WebSocket 连接逐帧解码并把事件批次交给调用方,支持 context 取消、可选帧上限、handler 错误传播、异常 close/读错后的 backoff 重连以及 `ReconnectAttempts < 0` 无限重连语义;该能力为 daemon 常驻 stream 托管接线打底。完整云端协议 hardening 仍未完成。
751751
- 本轮补充:`--daemon` 常驻模式现在会在初始 tick 后启动 remote WebSocket stream goroutine,按 heartbeat 间隔重试注册状态,复用 `StreamWebSocketEvents``RemoteTrigger` delivery/dedupe,把推送事件实时注入 running team,并在 `remote-pump.json` 中持续更新 `websocket_stream` transport、frame/connect/reconnect、delivered/duplicate/error 计数;daemon heartbeat/tick 在已注册 `websocket_url` 时会跳过短 WebSocket 读取,避免 stream 和 tick 双连接/重复写 pump state,poll-only 注册仍走原 tick 路径;daemon stop/context cancel 会取消 stream,并在 pump state 与 `/status show remote` 中记录 stream start/end/stop reason。完整云端协议 hardening 仍未完成。
752752
- 本轮补充:remote poll/WebSocket 共用解码器现在兼容 `data``event``remote_event``delivery``payload` 包裹的单条事件,以及这些 wrapper 下的 `events/items/messages/deliveries` 列表;云端可以用 envelope 协议携带 cursor 和事件内容,而无需强制把事件字段铺在顶层。更深的鉴权刷新、ack/lease 和服务端协议协商仍未完成。

internal/conversation/run.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1885,6 +1885,9 @@ func formatRemotePump(state remotepkg.PumpState) []string {
18851885
parts = append(parts, fmt.Sprintf("delivered %d", state.DeliveredCount))
18861886
parts = append(parts, fmt.Sprintf("duplicates %d", state.DuplicateCount))
18871887
parts = append(parts, fmt.Sprintf("errors %d", state.ErrorCount))
1888+
if state.CloseCode > 0 {
1889+
parts = append(parts, fmt.Sprintf("close %d", state.CloseCode))
1890+
}
18881891
if state.StreamStartedAt != "" {
18891892
parts = append(parts, "stream started "+state.StreamStartedAt)
18901893
}

internal/conversation/run_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2829,6 +2829,7 @@ func TestRunnerExecutesStatusShowSectionsWithoutQuery(t *testing.T) {
28292829
StreamEndedAt: "2026-06-17T10:08:00Z",
28302830
StreamStopReason: "context_cancelled",
28312831
StatusCode: http.StatusOK,
2832+
CloseCode: 1000,
28322833
FrameCount: 2,
28332834
ConnectCount: 1,
28342835
ReconnectCount: 1,
@@ -2978,6 +2979,7 @@ func TestRunnerExecutesStatusShowSectionsWithoutQuery(t *testing.T) {
29782979
"Remote environment: env-status",
29792980
"Remote registration: registered: url https://remote.example/register: status 202: remote session remote-status: websocket wss://remote.example/ws: poll https://remote.example/poll",
29802981
"Remote pump: running: transport websocket: websocket wss://remote.example/ws: poll https://remote.example/poll: cursor cursor-2: status 200: frames 2: connects 1: reconnects 1: events 3: delivered 2: duplicates 1: errors 0",
2982+
"close 1000",
29812983
"stream started 2026-06-17T10:03:00Z: stream ended 2026-06-17T10:08:00Z: stream stop context_cancelled",
29822984
"Remote services: 2",
29832985
"- bridge: running: endpoint http://127.0.0.1:8888: websocket ws://127.0.0.1:8888/ws: token required: commands 2: capabilities websocket_protocol, remote_trigger, remote_service",

internal/remote/pump.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ type PumpState struct {
3737
StreamEndedAt string `json:"stream_ended_at,omitempty"`
3838
StreamStopReason string `json:"stream_stop_reason,omitempty"`
3939
StatusCode int `json:"status_code,omitempty"`
40+
CloseCode int `json:"close_code,omitempty"`
4041
FrameCount int `json:"frame_count,omitempty"`
4142
ConnectCount int `json:"connect_count,omitempty"`
4243
ReconnectCount int `json:"reconnect_count,omitempty"`

internal/remote/pump_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ func TestWriteAndLoadPumpState(t *testing.T) {
121121
StreamStartedAt: "2026-06-17T10:00:00Z",
122122
StreamEndedAt: "2026-06-17T10:05:00Z",
123123
StreamStopReason: "max_frames",
124+
CloseCode: 1000,
124125
FrameCount: 2,
125126
ConnectCount: 1,
126127
ReconnectCount: 1,
@@ -134,7 +135,7 @@ func TestWriteAndLoadPumpState(t *testing.T) {
134135
if err != nil {
135136
t.Fatal(err)
136137
}
137-
if loaded.SessionID != "sess_remote" || loaded.RuntimeState != PumpRunning || loaded.Transport != "websocket" || loaded.WebSocketURL != "wss://remote/ws" || loaded.LastCursor != "cursor-1" || loaded.StreamStartedAt != "2026-06-17T10:00:00Z" || loaded.StreamEndedAt != "2026-06-17T10:05:00Z" || loaded.StreamStopReason != "max_frames" || loaded.FrameCount != 2 || loaded.ConnectCount != 1 || loaded.ReconnectCount != 1 || loaded.LastPollAt == "" {
138+
if loaded.SessionID != "sess_remote" || loaded.RuntimeState != PumpRunning || loaded.Transport != "websocket" || loaded.WebSocketURL != "wss://remote/ws" || loaded.LastCursor != "cursor-1" || loaded.StreamStartedAt != "2026-06-17T10:00:00Z" || loaded.StreamEndedAt != "2026-06-17T10:05:00Z" || loaded.StreamStopReason != "max_frames" || loaded.CloseCode != 1000 || loaded.FrameCount != 2 || loaded.ConnectCount != 1 || loaded.ReconnectCount != 1 || loaded.LastPollAt == "" {
138139
t.Fatalf("loaded = %#v", loaded)
139140
}
140141
data, err := json.Marshal(loaded)

0 commit comments

Comments
 (0)