corago/codex-keep/rpc.go
ms2k e4b84d9802 Add daemon-attached goal recovery operator
Resume goals after transient API failures with persisted exponential backoff while respecting pauses and interruptions.
2026-10-18 14:01:07 +09:00

126 lines
2.7 KiB
Go

package main
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"strconv"
"time"
"github.com/coder/websocket"
)
type frame struct {
message message
err error
}
type connection struct {
write func([]byte) error
frames chan frame
serial uint64
onEvent func(message) error
stop func()
}
func connect(ctx context.Context, socket string) (*connection, error) {
ctx, cancel := context.WithCancel(ctx)
transport := &http.Transport{DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
return (&net.Dialer{}).DialContext(ctx, "unix", socket)
}}
handshake, stop := context.WithTimeout(ctx, 5*time.Second)
defer stop()
ws, _, err := websocket.Dial(handshake, "ws://localhost/", &websocket.DialOptions{HTTPClient: &http.Client{Transport: transport}})
if err != nil {
cancel()
transport.CloseIdleConnections()
return nil, err
}
ws.SetReadLimit(-1)
c := &connection{frames: make(chan frame, 16)}
c.write = func(data []byte) error { return ws.Write(ctx, websocket.MessageText, data) }
done := make(chan struct{})
go func() {
defer close(done)
for {
_, data, err := ws.Read(ctx)
var m message
if err == nil {
err = json.Unmarshal(data, &m)
}
select {
case c.frames <- frame{m, err}:
case <-ctx.Done():
return
}
if err != nil {
return
}
}
}()
c.stop = func() {
cancel()
_ = ws.CloseNow()
transport.CloseIdleConnections()
<-done
}
return c, nil
}
func (c *connection) send(id uint64, method string, params any) error {
data, err := json.Marshal(struct {
ID uint64 `json:"id,omitempty"`
Method string `json:"method"`
Params any `json:"params"`
}{id, method, params})
if err != nil {
return err
}
return c.write(data)
}
func (c *connection) event(m message) error {
// Approval and input requests belong to the interactive clients.
if len(m.ID) != 0 || m.Method == "" || c.onEvent == nil {
return nil
}
return c.onEvent(m)
}
func (c *connection) call(ctx context.Context, method string, params, result any) error {
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
c.serial++
id := c.serial
if err := c.send(id, method, params); err != nil {
return err
}
for {
select {
case <-ctx.Done():
return ctx.Err()
case f := <-c.frames:
if f.err != nil {
return f.err
}
m := f.message
if m.Method == "" && string(m.ID) == strconv.FormatUint(id, 10) {
if m.Error != nil {
return m.Error
}
if result == nil {
return nil
}
if err := json.Unmarshal(m.Result, result); err != nil {
return fmt.Errorf("%s: %w", method, err)
}
return nil
}
if err := c.event(m); err != nil {
return err
}
}
}
}