Skip to content
Open
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
6 changes: 3 additions & 3 deletions internal/backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,10 @@ func (c *Client) IngestEvent(ctx context.Context, req *agentv1.ProcessHookEventR
return fmt.Errorf("send hook event: %w", err)
}
if err := stream.CloseRequest(); err != nil {
return err
return fmt.Errorf("close request: %w", err)
}
if resp, err := stream.Receive(); err == nil {
_ = resp
if _, err := stream.Receive(); err != nil {
return fmt.Errorf("receive response: %w", err)
}
return stream.CloseResponse()
}
Expand Down
12 changes: 9 additions & 3 deletions internal/run/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,10 @@ func Start(ctx context.Context, opts Options) error {
}

// 5. Start sidecar
sessionDir := filepath.Join(os.TempDir(), "kontext", sessionID)
// Use /tmp (not $TMPDIR) with a short ID to keep the Unix socket path
// under macOS's 104-byte sun_path limit. $TMPDIR on macOS is a long
// path like /var/folders/.../T/ which pushes the socket path over.
sessionDir := filepath.Join("/tmp", "kontext", truncateID(sessionID))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Session directory uses truncated 8-char ID, creating collision risk between concurrent sessions

The session directory is now derived from truncateID(sessionID) which uses only the first 8 characters of the session ID (internal/run/run.go:100). If two concurrent kontext start instances receive session IDs sharing the same 8-character prefix, they will share the same directory. This causes:

  1. The second session's sidecar.Start() removes the first session's socket (internal/sidecar/sidecar.go:49)
  2. Either session ending calls os.RemoveAll(sessionDir) (internal/run/run.go:138), deleting the other's socket and settings mid-operation

Critically, the truncation is unnecessary for the stated goal of staying under macOS's 104-byte sun_path limit. Switching from os.TempDir() to /tmp already saves ~35+ characters. Using the full session ID gives a path of ~62 bytes (/tmp/kontext/<UUID>/kontext.sock), well under the 104-byte limit. The old code used the full sessionID, which guaranteed uniqueness.

Suggested change
sessionDir := filepath.Join("/tmp", "kontext", truncateID(sessionID))
sessionDir := filepath.Join("/tmp", "kontext", sessionID)
Open in Devin Review

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

os.MkdirAll(sessionDir, 0700)

sc, err := sidecar.New(sessionDir, client, sessionID, opts.Agent)
Expand Down Expand Up @@ -126,8 +129,11 @@ func Start(ctx context.Context, opts Options) error {
endCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

_ = client.EndSession(endCtx, sessionID)
fmt.Fprintf(os.Stderr, "\n✓ Session ended (%s)\n", truncateID(sessionID))
if err := client.EndSession(endCtx, sessionID); err != nil {
fmt.Fprintf(os.Stderr, "\n⚠ Could not end session on backend: %v\n", err)
} else {
fmt.Fprintf(os.Stderr, "\n✓ Session ended (%s)\n", truncateID(sessionID))
}

os.RemoveAll(sessionDir)

Expand Down
22 changes: 20 additions & 2 deletions internal/sidecar/sidecar.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ package sidecar

import (
"context"
"fmt"
"log"
"net"
"os"
"path/filepath"
"sync"
"sync/atomic"
"time"

agentv1 "github.com/kontext-dev/kontext-cli/gen/kontext/agent/v1"
Expand All @@ -23,6 +26,9 @@ type Server struct {
agentName string
client *backend.Client
cancel context.CancelFunc

ingestFails atomic.Int64
ingestWarnOnce sync.Once
}

// New creates a new sidecar server.
Expand Down Expand Up @@ -120,20 +126,32 @@ func (s *Server) ingestEvent(ctx context.Context, req *EvaluateRequest) {
}

if err := s.client.IngestEvent(ctx, hookEvent); err != nil {
log.Printf("sidecar: ingest: %v", err)
n := s.ingestFails.Add(1)
s.ingestWarnOnce.Do(func() {
fmt.Fprintf(os.Stderr, "sidecar: event ingestion failed: %v\n", err)
})
if n > 1 && n%10 == 0 {
fmt.Fprintf(os.Stderr, "sidecar: %d event ingestion failures (latest: %v)\n", n, err)
}
}
}

func (s *Server) heartbeatLoop(ctx context.Context) {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
consecutiveFails := 0
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
if err := s.client.Heartbeat(ctx, s.sessionID); err != nil {
log.Printf("sidecar: heartbeat: %v", err)
consecutiveFails++
if consecutiveFails == 1 || consecutiveFails == 3 {
fmt.Fprintf(os.Stderr, "sidecar: heartbeat failed (attempt %d): %v\n", consecutiveFails, err)
}
} else {
consecutiveFails = 0
}
}
}
Expand Down
Loading