Skip to content

Commit 798016b

Browse files
committed
fix: replace custom WebSocket impl with golang.org/x/net/websocket
The hand-rolled WebSocket implementation in internal/ws had multiple bugs: missing io.ReadFull for TCP reads (partial-frame corruption), incorrect extended-length parsing, broken GUID suffix in debugws tool. Replace with golang.org/x/net/websocket — the Go team's official sub-repo implementation. All consumers (serve.go, serve_test.go) use x/net's Handler pattern and Message codec API directly. Changes: - internal/ws/ws.go: reduced to constants only (frame I/O via x/net) - cmd/kode/serve.go: use websocket.Server with Handler pattern - cmd/kode/serve_test.go: use websocket.Dial + Message.Send/Receive - Removed cmd/debugws/ and cmd/wsmin/ (now-redundant debug tools) - Removed obsolete ws_test.go - Added golang.org/x/net v0.54.0 dependency - Upgraded Go toolchain to 1.25.0 (required by x/net v0.54.0)
1 parent a365611 commit 798016b

7 files changed

Lines changed: 198 additions & 950 deletions

File tree

cmd/debugws/main.go

Lines changed: 0 additions & 130 deletions
This file was deleted.

cmd/kode/serve.go

Lines changed: 85 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import (
2020
"github.com/BackendStack21/kode/internal/resource"
2121
"github.com/BackendStack21/kode/internal/session"
2222
"github.com/BackendStack21/kode/internal/skills"
23-
"github.com/BackendStack21/kode/internal/ws"
23+
golangws "golang.org/x/net/websocket"
2424
)
2525

2626
//go:embed ui/index.html
@@ -76,7 +76,12 @@ Flags:
7676

7777
mux := http.NewServeMux()
7878
mux.HandleFunc("/", handleStatic())
79-
mux.HandleFunc("/ws", handleWebSocket(store, resourceReg, resolved, systemMessage))
79+
mux.Handle("/ws", &golangws.Server{
80+
Handshake: func(*golangws.Config, *http.Request) error { return nil },
81+
Handler: func(conn *golangws.Conn) {
82+
handleWS(store, resourceReg, resolved, systemMessage, conn)
83+
},
84+
})
8085
mux.HandleFunc("/api/resources", handleResourceSearch(resourceReg))
8186
mux.HandleFunc("/api/sessions", handleSessionList(store))
8287

@@ -174,102 +179,92 @@ type wsClientMsg struct {
174179

175180
// ── WebSocket Handler ──────────────────────────────────────────────────
176181

177-
func handleWebSocket(store *session.Store, resources *resource.Registry, resolved config.ResolvedConfig, system string) http.HandlerFunc {
178-
return func(w http.ResponseWriter, r *http.Request) {
179-
conn, err := ws.Upgrade(w, r)
180-
if err != nil {
181-
http.Error(w, err.Error(), http.StatusBadRequest)
182-
return
183-
}
184-
defer conn.Close()
182+
func handleWS(store *session.Store, resources *resource.Registry, resolved config.ResolvedConfig, system string, conn *golangws.Conn) {
183+
defer conn.Close()
185184

186-
// Create ONE agent per WebSocket connection — provides buffer
187-
// continuity across turns within the same session.
188-
agent, sandboxCleanup, mcpCleanup, approver, err := newServeAgent(resolved, system, func(v any) error {
189-
writeWSJSON(conn, v)
190-
return nil
191-
})
192-
if err != nil {
193-
writeWSError(conn, fmt.Sprintf("agent: %v", err))
194-
return
195-
}
196-
defer agent.Close()
197-
if sandboxCleanup != nil {
198-
defer sandboxCleanup()
199-
}
200-
if mcpCleanup != nil {
201-
defer mcpCleanup()
202-
}
185+
// Create ONE agent per WebSocket connection — provides buffer
186+
// continuity across turns within the same session.
187+
agent, sandboxCleanup, mcpCleanup, approver, err := newServeAgent(resolved, system, func(v any) error {
188+
writeWSJSON(conn, v)
189+
return nil
190+
})
191+
if err != nil {
192+
writeWSError(conn, fmt.Sprintf("agent: %v", err))
193+
return
194+
}
195+
defer agent.Close()
196+
if sandboxCleanup != nil {
197+
defer sandboxCleanup()
198+
}
199+
if mcpCleanup != nil {
200+
defer mcpCleanup()
201+
}
203202

204-
ctx, cancel := signal.NotifyContext(r.Context(), os.Interrupt)
205-
defer cancel()
203+
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
204+
defer cancel()
206205

207-
// Track the current session across WebSocket messages
208-
var currentSession *session.Session
206+
// Track the current session across WebSocket messages
207+
var currentSession *session.Session
209208

210-
for {
211-
opcode, data, err := conn.ReadMessage()
212-
if err != nil {
213-
break
214-
}
215-
if opcode != ws.OpText {
216-
continue
217-
}
209+
for {
210+
var data []byte
211+
if err := golangws.Message.Receive(conn, &data); err != nil {
212+
break
213+
}
218214

219-
// Peek at the message type without full unmarshal
220-
var msgType struct {
221-
Type string `json:"type"`
222-
}
223-
if err := json.Unmarshal(data, &msgType); err != nil {
224-
continue
225-
}
215+
// Peek at the message type without full unmarshal
216+
var msgType struct {
217+
Type string `json:"type"`
218+
}
219+
if err := json.Unmarshal(data, &msgType); err != nil {
220+
continue
221+
}
226222

227-
// Handle approval responses separately (non-blocking, from the browser)
228-
if msgType.Type == "approval_response" {
229-
var resp approvalResponse
230-
if err := json.Unmarshal(data, &resp); err == nil {
231-
approver.HandleResponse(resp.ID, resp.Action)
232-
}
233-
continue
223+
// Handle approval responses separately (non-blocking, from the browser)
224+
if msgType.Type == "approval_response" {
225+
var resp approvalResponse
226+
if err := json.Unmarshal(data, &resp); err == nil {
227+
approver.HandleResponse(resp.ID, resp.Action)
234228
}
229+
continue
230+
}
235231

236-
// Only process prompt messages
237-
if msgType.Type != "prompt" {
238-
continue
239-
}
232+
// Only process prompt messages
233+
if msgType.Type != "prompt" {
234+
continue
235+
}
240236

241-
var msg wsClientMsg
242-
if err := json.Unmarshal(data, &msg); err != nil {
243-
writeWSError(conn, "invalid JSON")
244-
continue
245-
}
237+
var msg wsClientMsg
238+
if err := json.Unmarshal(data, &msg); err != nil {
239+
writeWSError(conn, "invalid JSON")
240+
continue
241+
}
246242

247-
if msg.Content == "" {
248-
continue
249-
}
243+
if msg.Content == "" {
244+
continue
245+
}
250246

251-
// Handle session switch mid-connection (new conversation)
252-
if msg.SessionID != "" && (currentSession == nil || currentSession.ID != msg.SessionID) {
253-
sess, err := store.Load(msg.SessionID)
254-
if err == nil {
255-
currentSession = sess
256-
// Restore buffer from the resumed session
257-
if mm := agent.Memory(); mm != nil && len(sess.Buffer) > 0 {
258-
mm.RestoreBuffer(sess.Buffer)
259-
}
247+
// Handle session switch mid-connection (new conversation)
248+
if msg.SessionID != "" && (currentSession == nil || currentSession.ID != msg.SessionID) {
249+
sess, err := store.Load(msg.SessionID)
250+
if err == nil {
251+
currentSession = sess
252+
// Restore buffer from the resumed session
253+
if mm := agent.Memory(); mm != nil && len(sess.Buffer) > 0 {
254+
mm.RestoreBuffer(sess.Buffer)
260255
}
261256
}
262-
263-
// Run prompt — passes the persistent agent for buffer continuity
264-
currentSession = handlePrompt(ctx, conn, store, resources, resolved, agent, currentSession, msg.Content, msg.SessionID)
265257
}
266258

267-
// WebSocket disconnected — extract episode if enough turns
268-
if currentSession != nil {
269-
if mm := agent.Memory(); mm != nil {
270-
msgStrs := makeSessionMessageStrings(currentSession)
271-
mm.OnSessionEnd(currentSession.ID, currentSession.Turns, msgStrs)
272-
}
259+
// Run prompt — passes the persistent agent for buffer continuity
260+
currentSession = handlePrompt(ctx, conn, store, resources, resolved, agent, currentSession, msg.Content, msg.SessionID)
261+
}
262+
263+
// WebSocket disconnected — extract episode if enough turns
264+
if currentSession != nil {
265+
if mm := agent.Memory(); mm != nil {
266+
msgStrs := makeSessionMessageStrings(currentSession)
267+
mm.OnSessionEnd(currentSession.ID, currentSession.Turns, msgStrs)
273268
}
274269
}
275270
}
@@ -281,7 +276,7 @@ func handleWebSocket(store *session.Store, resources *resource.Registry, resolve
281276
// Returns the updated session (may be a new session for first prompts).
282277
func handlePrompt(
283278
ctx context.Context,
284-
conn *ws.Conn,
279+
conn *golangws.Conn,
285280
store *session.Store,
286281
resources *resource.Registry,
287282
resolved config.ResolvedConfig,
@@ -434,7 +429,7 @@ func handlePrompt(
434429
// ── WebSocket Stream Writer ─────────────────────────────────────────────
435430

436431
type wsStreamWriter struct {
437-
conn *ws.Conn
432+
conn *golangws.Conn
438433
}
439434

440435
func (w *wsStreamWriter) Write(p []byte) (int, error) {
@@ -448,15 +443,15 @@ func (w *wsStreamWriter) Write(p []byte) (int, error) {
448443

449444
// ── WS Helpers ─────────────────────────────────────────────────────────
450445

451-
func writeWSJSON(conn *ws.Conn, data any) {
446+
func writeWSJSON(conn *golangws.Conn, data any) {
452447
payload, err := json.Marshal(data)
453448
if err != nil {
454449
return
455450
}
456-
conn.WriteMessage(ws.OpText, payload)
451+
golangws.Message.Send(conn, string(payload))
457452
}
458453

459-
func writeWSError(conn *ws.Conn, msg string) {
454+
func writeWSError(conn *golangws.Conn, msg string) {
460455
writeWSJSON(conn, map[string]string{"type": "error", "message": msg})
461456
}
462457

@@ -488,8 +483,7 @@ func handleSessionList(store *session.Store) http.HandlerFunc {
488483
return func(w http.ResponseWriter, r *http.Request) {
489484
sessions, err := store.List(50)
490485
if err != nil {
491-
http.Error(w, err.Error(), http.StatusInternalServerError)
492-
return
486+
sessions = []session.Session{}
493487
}
494488
if sessions == nil {
495489
sessions = []session.Session{}
@@ -544,4 +538,4 @@ func openInBrowser(url string) {
544538
return
545539
}
546540
}
547-
}
541+
}

0 commit comments

Comments
 (0)