Compare commits

...

1 Commits

Author SHA1 Message Date
Jiayuan Zhang
3c97efc2e4 fix(daemon): symlink Codex sessions dir to shared home for discoverability
Per-task CODEX_HOME isolated session logs in per-task directories, making
them invisible from the global ~/.codex/sessions/ where users expect to
find them. Symlink the sessions directory back to the shared home so
Codex writes session logs to the global location while keeping skills
isolated per task.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 14:56:33 +08:00
2 changed files with 68 additions and 4 deletions

View File

@@ -8,6 +8,13 @@ import (
"path/filepath"
)
// Directories to symlink from the shared ~/.codex/ into the per-task CODEX_HOME.
// The shared directory is created if it doesn't exist, ensuring Codex session
// logs are always written to the global home where users can find them.
var codexSymlinkedDirs = []string{
"sessions",
}
// Files to symlink from the shared ~/.codex/ into the per-task CODEX_HOME.
// Symlinks share state (e.g. auth tokens) so changes propagate automatically.
var codexSymlinkedFiles = []string{
@@ -32,6 +39,15 @@ func prepareCodexHome(codexHome string, logger *slog.Logger) error {
return fmt.Errorf("create codex-home dir: %w", err)
}
// Symlink shared directories (sessions) so logs stay in the global home.
for _, name := range codexSymlinkedDirs {
src := filepath.Join(sharedHome, name)
dst := filepath.Join(codexHome, name)
if err := ensureDirSymlink(src, dst); err != nil {
logger.Warn("execenv: codex-home dir symlink failed", "dir", name, "error", err)
}
}
// Symlink shared files (auth).
for _, name := range codexSymlinkedFiles {
src := filepath.Join(sharedHome, name)
@@ -69,6 +85,31 @@ func resolveSharedCodexHome() string {
return filepath.Join(home, ".codex")
}
// ensureDirSymlink creates a symlink dst → src for a directory.
// Unlike ensureSymlink, it creates the source directory if it doesn't exist,
// so Codex can write to it immediately.
func ensureDirSymlink(src, dst string) error {
if err := os.MkdirAll(src, 0o755); err != nil {
return fmt.Errorf("create shared dir %s: %w", src, err)
}
// Check if dst already exists.
if fi, err := os.Lstat(dst); err == nil {
if fi.Mode()&os.ModeSymlink != 0 {
target, err := os.Readlink(dst)
if err == nil && target == src {
return nil // already correct
}
os.Remove(dst)
} else {
// Regular file/dir exists — don't overwrite.
return nil
}
}
return os.Symlink(src, dst)
}
// ensureSymlink creates a symlink dst → src. If src doesn't exist, it's a no-op.
// If dst already exists as a correct symlink, it's a no-op. If dst is a broken
// symlink, it's replaced.

View File

@@ -617,9 +617,23 @@ func TestPrepareCodexHomeSeedsFromShared(t *testing.T) {
t.Fatalf("prepareCodexHome failed: %v", err)
}
// sessions should be a symlink to the shared sessions dir.
sessionsPath := filepath.Join(codexHome, "sessions")
fi, err := os.Lstat(sessionsPath)
if err != nil {
t.Fatalf("sessions not found: %v", err)
}
if fi.Mode()&os.ModeSymlink == 0 {
t.Error("sessions should be a symlink")
}
sessTarget, _ := os.Readlink(sessionsPath)
if sessTarget != filepath.Join(sharedHome, "sessions") {
t.Errorf("sessions symlink target = %q, want %q", sessTarget, filepath.Join(sharedHome, "sessions"))
}
// auth.json should be a symlink.
authPath := filepath.Join(codexHome, "auth.json")
fi, err := os.Lstat(authPath)
fi, err = os.Lstat(authPath)
if err != nil {
t.Fatalf("auth.json not found: %v", err)
}
@@ -675,17 +689,26 @@ func TestPrepareCodexHomeSkipsMissingFiles(t *testing.T) {
t.Fatalf("prepareCodexHome failed: %v", err)
}
// Directory should exist but be empty (no auth.json, no config.json, etc.).
// Directory should only contain the sessions symlink (no auth.json, no config.json, etc.).
entries, err := os.ReadDir(codexHome)
if err != nil {
t.Fatalf("failed to read codex-home: %v", err)
}
if len(entries) != 0 {
if len(entries) != 1 {
names := make([]string, len(entries))
for i, e := range entries {
names[i] = e.Name()
}
t.Errorf("expected empty codex-home, got: %v", names)
t.Errorf("expected only sessions symlink in codex-home, got: %v", names)
}
// sessions should be a symlink to the shared sessions dir.
sessionsPath := filepath.Join(codexHome, "sessions")
fi, err := os.Lstat(sessionsPath)
if err != nil {
t.Fatalf("sessions not found: %v", err)
}
if fi.Mode()&os.ModeSymlink == 0 {
t.Error("sessions should be a symlink")
}
}