From c00ffb42f206585ec36cf02a55ff735b93b6d99d Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 28 May 2026 09:57:43 +0000 Subject: [PATCH] Route Codex approvals to Telegram Force app-server turns to use the user approval reviewer so command approvals surface in the bot on Codex 0.134. Add focused protocol logs for approval requests and guardian review events to diagnose silent approval stalls. --- cmd/bot/main.go | 1 + internal/codexapp/client.go | 51 +++++++++++++++++++++++++++----- internal/codexapp/client_test.go | 34 +++++++++++++++++++++ internal/telegram/bot.go | 50 +++++++++++++++++++++++++++++++ 4 files changed, 129 insertions(+), 7 deletions(-) diff --git a/cmd/bot/main.go b/cmd/bot/main.go index 4516300..08065f0 100644 --- a/cmd/bot/main.go +++ b/cmd/bot/main.go @@ -34,6 +34,7 @@ func main() { tg := telegram.NewClient(cfg.TelegramToken) codex := codexapp.New(cfg.CodexSocketPath, cfg.AppVersion) + codex.SetLogger(logger) defer codex.Close() bot := telegram.NewBot(tg, st, codex, cfg.UploadDir, cfg.CodexHome, cfg.CodexStateDB, cfg.DefaultModel, cfg.DefaultSandbox, cfg.PollTimeout, logger) diff --git a/internal/codexapp/client.go b/internal/codexapp/client.go index 3d741e1..3d15bdc 100644 --- a/internal/codexapp/client.go +++ b/internal/codexapp/client.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "log" "net" "net/http" "strconv" @@ -26,6 +27,7 @@ type Client struct { pending map[string]chan rpcResult events chan Event connected bool + logger *log.Logger } type Event struct { @@ -151,6 +153,12 @@ func New(socketPath, version string) *Client { } } +func (c *Client) SetLogger(logger *log.Logger) { + c.mu.Lock() + defer c.mu.Unlock() + c.logger = logger +} + func (c *Client) Events() <-chan Event { return c.events } @@ -240,10 +248,11 @@ func (c *Client) StartThread(ctx context.Context, cwd, model, sandbox string) (T return Thread{}, err } params := map[string]any{ - "cwd": cwd, - "approvalPolicy": "on-request", - "sandbox": threadSandbox(sandbox), - "serviceName": "codex_telegram_bot", + "cwd": cwd, + "approvalPolicy": "on-request", + "approvalsReviewer": "user", + "sandbox": threadSandbox(sandbox), + "serviceName": "codex_telegram_bot", } if model != "" { params["model"] = model @@ -327,9 +336,10 @@ func (c *Client) StartTurn(ctx context.Context, threadID, cwd, model, reasoningE return Turn{}, err } params := map[string]any{ - "threadId": threadID, - "input": input, - "approvalPolicy": "on-request", + "threadId": threadID, + "input": input, + "approvalPolicy": "on-request", + "approvalsReviewer": "user", } if strings.TrimSpace(cwd) != "" { params["cwd"] = cwd @@ -463,6 +473,7 @@ func (c *Client) readLoop(conn *websocket.Conn) { return } if env.Method != "" && hasID { + c.logProtocolEvent("server request", env.Method, id.Key(), env.Params) c.events <- Event{ ID: &id, Method: env.Method, @@ -476,6 +487,9 @@ func (c *Client) readLoop(conn *websocket.Conn) { continue } if env.Method != "" { + if shouldLogNotification(env.Method) { + c.logProtocolEvent("notification", env.Method, "", env.Params) + } c.events <- Event{ Method: env.Method, Params: env.Params, @@ -484,6 +498,29 @@ func (c *Client) readLoop(conn *websocket.Conn) { } } +func (c *Client) logProtocolEvent(kind, method, id string, params json.RawMessage) { + c.mu.Lock() + logger := c.logger + c.mu.Unlock() + if logger == nil { + return + } + if id != "" { + logger.Printf("codex %s: method=%s id=%s params_bytes=%d", kind, method, id, len(params)) + return + } + logger.Printf("codex %s: method=%s params_bytes=%d", kind, method, len(params)) +} + +func shouldLogNotification(method string) bool { + switch method { + case "item/guardianApprovalReview/started", "item/guardianApprovalReview/completed", "guardianWarning", "serverRequest/resolved": + return true + default: + return false + } +} + func (c *Client) completeCall(id RequestID, result json.RawMessage, rpcErr *RPCError) { c.mu.Lock() ch := c.pending[id.Key()] diff --git a/internal/codexapp/client_test.go b/internal/codexapp/client_test.go index 3cb18b0..5886590 100644 --- a/internal/codexapp/client_test.go +++ b/internal/codexapp/client_test.go @@ -77,6 +77,12 @@ func TestClientWebSocketUnixJSONRPC(t *testing.T) { serverDone <- unexpectedMessage("thread/start", start["method"]) return } + startParams := start["params"].(map[string]any) + if startParams["approvalsReviewer"] != "user" || startParams["approvalPolicy"] != "on-request" { + payload, _ := json.Marshal(startParams) + serverDone <- unexpectedMessage("thread/start approval params", string(payload)) + return + } if err := conn.WriteJSON(map[string]any{ "id": start["id"], "result": map[string]any{ @@ -88,6 +94,26 @@ func TestClientWebSocketUnixJSONRPC(t *testing.T) { return } + var turnStart map[string]any + if err := conn.ReadJSON(&turnStart); err != nil { + serverDone <- err + return + } + if turnStart["method"] != "turn/start" { + serverDone <- unexpectedMessage("turn/start", turnStart["method"]) + return + } + turnParams := turnStart["params"].(map[string]any) + if turnParams["approvalsReviewer"] != "user" || turnParams["approvalPolicy"] != "on-request" { + payload, _ := json.Marshal(turnParams) + serverDone <- unexpectedMessage("turn/start approval params", string(payload)) + return + } + if err := conn.WriteJSON(map[string]any{"id": turnStart["id"], "result": map[string]any{"turn": map[string]any{"id": "turn_1", "status": "running"}}}); err != nil { + serverDone <- err + return + } + var readThread map[string]any if err := conn.ReadJSON(&readThread); err != nil { serverDone <- err @@ -174,6 +200,14 @@ func TestClientWebSocketUnixJSONRPC(t *testing.T) { if thread.ID != "thr_1" || thread.CWD != projectCWD { t.Fatalf("unexpected thread: %+v", thread) } + turn, err := client.StartTurn(ctx, "thr_1", projectCWD, "", "", "workspace-write", []InputItem{{Type: "text", Text: "hello"}}) + if err != nil { + t.Fatal(err) + } + if turn.ID != "turn_1" { + t.Fatalf("unexpected turn: %+v", turn) + } + readThread, err := client.ReadThread(ctx, "thr_1") if err != nil { t.Fatal(err) diff --git a/internal/telegram/bot.go b/internal/telegram/bot.go index d3290b1..4314f9b 100644 --- a/internal/telegram/bot.go +++ b/internal/telegram/bot.go @@ -1266,6 +1266,15 @@ func parseCodexThreadItem(raw json.RawMessage) (codexThreadItemView, error) { return item, nil } +func renderGuardianReviewHTML(title string, raw json.RawMessage) string { + var params map[string]any + if err := json.Unmarshal(raw, ¶ms); err != nil { + return SummaryDetailsHTML(title, string(raw)) + } + sections := renderSelectedArgumentDetailsHTML(params, []string{"action", "review", "decisionSource", "targetItemId", "reviewId"}) + return SummaryRawHTMLSectionsLimited(title, sections, TelegramHTMLMessageLimit) +} + func renderCodexItemStarted(item codexThreadItemView) string { switch item.Type { case "commandExecution": @@ -1702,6 +1711,40 @@ func (b *Bot) handleCodexNotification(ctx context.Context, event codexapp.Event) return err } return b.sendImageOutput(ctx, params.ThreadID, item) + case "item/guardianApprovalReview/started", "item/guardianApprovalReview/completed": + var params struct { + ThreadID string `json:"threadId"` + TurnID string `json:"turnId"` + ReviewID string `json:"reviewId"` + TargetItemID *string `json:"targetItemId"` + } + if err := json.Unmarshal(event.Params, ¶ms); err != nil { + return err + } + targetItemID := "" + if params.TargetItemID != nil { + targetItemID = *params.TargetItemID + } + b.logger.Printf("codex guardian approval review: method=%s thread=%s turn=%s review=%s target=%s", event.Method, params.ThreadID, params.TurnID, params.ReviewID, targetItemID) + if params.ThreadID != "" && b.shouldHandleOutputEvent(params.ThreadID, params.TurnID) { + title := "Codex approval auto-review started" + if event.Method == "item/guardianApprovalReview/completed" { + title = "Codex approval auto-review completed" + } + return b.sendOutputHTMLBlock(ctx, params.ThreadID, renderGuardianReviewHTML(title, event.Params)) + } + case "guardianWarning": + var params struct { + ThreadID string `json:"threadId"` + Message string `json:"message"` + } + if err := json.Unmarshal(event.Params, ¶ms); err != nil { + return err + } + b.logger.Printf("codex guardian warning: thread=%s message=%q", params.ThreadID, truncateForStatus(params.Message)) + if params.ThreadID != "" && b.hasOutputThread(params.ThreadID) { + return b.sendOutputBlock(ctx, params.ThreadID, "Codex warning: "+params.Message) + } case "turn/diff/updated": var params struct { ThreadID string `json:"threadId"` @@ -1830,6 +1873,7 @@ func (b *Bot) handleCodexServerRequest(ctx context.Context, event codexapp.Event return errors.New("approval request missing threadId") } itemID := firstNonEmpty(params.ApprovalID, params.ItemID, params.CallID) + b.logger.Printf("codex approval request: method=%s request=%s thread=%s turn=%s item=%s approval=%s call=%s", event.Method, event.ID.Key(), threadID, params.TurnID, params.ItemID, params.ApprovalID, params.CallID) thread, err := b.store.GetThreadByCodexID(ctx, threadID) if err != nil { return err @@ -1911,6 +1955,12 @@ func (b *Bot) setOutputTurnID(threadID, turnID string) { } } +func (b *Bot) hasOutputThread(threadID string) bool { + b.mu.Lock() + defer b.mu.Unlock() + return b.outputs[threadID] != nil +} + func (b *Bot) hasOutputTurn(threadID, turnID string) bool { b.mu.Lock() defer b.mu.Unlock()