Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions commands/cc_statusline.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,24 @@ func commandCCStatusline(c *cli.Context) error {
var result ccStatuslineResult
config, err := configService.ReadConfigFile(ctx)
if err == nil {
// Send session-project mapping via daemon socket (fire-and-forget, ~1ms)
if data.SessionID != "" {
projectPath := ""
if data.Workspace != nil {
projectPath = data.Workspace.CurrentDir
if projectPath == "" {
projectPath = data.Workspace.ProjectDir
}
}
if projectPath != "" {
socketPath := config.SocketPath
if socketPath == "" {
socketPath = model.DefaultSocketPath
}
daemon.SendSessionProject(socketPath, data.SessionID, projectPath)
}
}

result = getDaemonInfoWithFallback(ctx, config, data.Cwd)
}

Expand Down
19 changes: 19 additions & 0 deletions daemon/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,25 @@ func SendLocalDataToSocket(
return nil
}

// SendSessionProject sends a session-to-project mapping to the daemon (fire-and-forget)
func SendSessionProject(socketPath string, sessionID, projectPath string) {
conn, err := net.DialTimeout("unix", socketPath, 10*time.Millisecond)
if err != nil {
return
}
defer conn.Close()

msg := SocketMessage{
Type: SocketMessageTypeSessionProject,
Payload: SessionProjectRequest{
SessionID: sessionID,
ProjectPath: projectPath,
},
}

json.NewEncoder(conn).Encode(msg)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The error returned by json.NewEncoder(conn).Encode(msg) is not handled. Even for a fire-and-forget function, it's good practice to log such errors for debugging purposes. You could use the slog package for this, which is used elsewhere in the daemon package (you would need to add the log/slog import).

}
Comment on lines +55 to +71
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Missing write deadline on socket connection in SendSessionProject can block the statusline indefinitely

SendSessionProject sets a 10ms dial timeout but never sets a write deadline on the connection. The json.NewEncoder(conn).Encode(msg) call at line 70 could block indefinitely if the daemon accepts the connection but is slow to read.

Root Cause and Impact

Compare with RequestCCInfo at daemon/client.go:82 which properly sets conn.SetDeadline(time.Now().Add(timeout)) before writing. SendSessionProject omits this safeguard.

This function is called synchronously in the statusline command (commands/cc_statusline.go:83) which has a hard 100ms timeout. If the daemon's read buffer is full or the daemon is busy, the write could block for much longer than the expected ~1ms, potentially consuming the entire 100ms budget and causing the statusline to time out. While Unix domain socket writes are typically fast due to kernel buffering, this is a missing safeguard that violates the "fire-and-forget, ~1ms" contract stated in the comment.

Impact: Under load or unusual daemon conditions, the statusline could hang or time out, degrading the user experience.

Suggested change
func SendSessionProject(socketPath string, sessionID, projectPath string) {
conn, err := net.DialTimeout("unix", socketPath, 10*time.Millisecond)
if err != nil {
return
}
defer conn.Close()
msg := SocketMessage{
Type: SocketMessageTypeSessionProject,
Payload: SessionProjectRequest{
SessionID: sessionID,
ProjectPath: projectPath,
},
}
json.NewEncoder(conn).Encode(msg)
}
func SendSessionProject(socketPath string, sessionID, projectPath string) {
conn, err := net.DialTimeout("unix", socketPath, 10*time.Millisecond)
if err != nil {
return
}
defer conn.Close()
conn.SetDeadline(time.Now().Add(10 * time.Millisecond))
msg := SocketMessage{
Type: SocketMessageTypeSessionProject,
Payload: SessionProjectRequest{
SessionID: sessionID,
ProjectPath: projectPath,
},
}
json.NewEncoder(conn).Encode(msg)
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


// RequestCCInfo requests CC info (cost data and git info) from the daemon
func RequestCCInfo(socketPath string, timeRange CCInfoTimeRange, workingDir string, timeout time.Duration) (*CCInfoResponse, error) {
conn, err := net.DialTimeout("unix", socketPath, timeout)
Expand Down
24 changes: 20 additions & 4 deletions daemon/socket.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package daemon

import (
"context"
"encoding/json"
"fmt"
"log/slog"
Expand All @@ -17,12 +18,18 @@ import (
type SocketMessageType string

const (
SocketMessageTypeSync SocketMessageType = "sync"
SocketMessageTypeHeartbeat SocketMessageType = "heartbeat"
SocketMessageTypeStatus SocketMessageType = "status"
SocketMessageTypeCCInfo SocketMessageType = "cc_info"
SocketMessageTypeSync SocketMessageType = "sync"
SocketMessageTypeHeartbeat SocketMessageType = "heartbeat"
SocketMessageTypeStatus SocketMessageType = "status"
SocketMessageTypeCCInfo SocketMessageType = "cc_info"
SocketMessageTypeSessionProject SocketMessageType = "session_project"
)

type SessionProjectRequest struct {
SessionID string `json:"sessionId"`
ProjectPath string `json:"projectPath"`
}

type CCInfoTimeRange string

const (
Expand Down Expand Up @@ -179,6 +186,15 @@ func (p *SocketHandler) handleConnection(conn net.Conn) {
encoder.Encode(map[string]string{"status": "ok"})
case SocketMessageTypeCCInfo:
p.handleCCInfo(conn, msg)
case SocketMessageTypeSessionProject:
if payload, ok := msg.Payload.(map[string]interface{}); ok {
sessionID, _ := payload["sessionId"].(string)
projectPath, _ := payload["projectPath"].(string)
if sessionID != "" && projectPath != "" {
go model.SendSessionProjectUpdate(context.Background(), *p.config, sessionID, projectPath)
slog.Debug("session_project update dispatched", slog.String("sessionId", sessionID))
}
Comment on lines +191 to +196
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

This block has a couple of issues that should be addressed for robustness:

  1. The type assertions for sessionID and projectPath ignore the ok boolean return value. This is unsafe because if the payload is malformed (e.g., a field is missing or has the wrong type), this will fail silently.
  2. The error returned by model.SendSessionProjectUpdate inside the goroutine is ignored. Any error during the API call to the server will be lost, making debugging difficult.

The suggested change below addresses both points by checking the type assertion results and handling the error from the API call by logging it.

			sessionID, ok1 := payload["sessionId"].(string)
			projectPath, ok2 := payload["projectPath"].(string)
			if ok1 && ok2 && sessionID != "" && projectPath != "" {
				go func() {
					if err := model.SendSessionProjectUpdate(context.Background(), *p.config, sessionID, projectPath); err != nil {
						slog.Warn("Failed to send session-project update", slog.Any("err", err), slog.String("sessionId", sessionID))
					}
				}()
				slog.Debug("session_project update dispatched", slog.String("sessionId", sessionID))
			} else {
				slog.Warn("Received invalid or incomplete session_project payload", "payload", payload)
			}

}
default:
slog.Error("Unknown message type:", slog.String("messageType", string(msg.Type)))
}
Expand Down
39 changes: 39 additions & 0 deletions model/api_session_project.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package model

import (
"context"
"net/http"
"time"
)

type sessionProjectRequest struct {
SessionID string `json:"session_id"`
ProjectPath string `json:"project_path"`
}

type sessionProjectResponse struct{}

// SendSessionProjectUpdate sends a session-to-project path mapping to the server
func SendSessionProjectUpdate(ctx context.Context, config ShellTimeConfig, sessionID, projectPath string) error {
ctx, span := modelTracer.Start(ctx, "session_project.send")
defer span.End()

var resp sessionProjectResponse
err := SendHTTPRequestJSON(HTTPRequestOptions[*sessionProjectRequest, sessionProjectResponse]{
Context: ctx,
Endpoint: Endpoint{
APIEndpoint: config.APIEndpoint,
Token: config.Token,
},
Method: http.MethodPost,
Path: "/api/v1/cc/session-project",
Payload: &sessionProjectRequest{
SessionID: sessionID,
ProjectPath: projectPath,
},
Response: &resp,
Timeout: 5 * time.Second,
})

return err
}
8 changes: 8 additions & 0 deletions model/cc_statusline_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ type CCStatuslineInput struct {
ContextWindow CCStatuslineContextWindow `json:"context_window"`
Cwd string `json:"cwd"`
Version string `json:"version"`
SessionID string `json:"session_id"`
Workspace *CCStatuslineWorkspace `json:"workspace"`
}

// CCStatuslineWorkspace represents workspace information from Claude Code
type CCStatuslineWorkspace struct {
CurrentDir string `json:"current_dir"`
ProjectDir string `json:"project_dir"`
}

// CCStatuslineModel represents model information
Expand Down
Loading