Skip to content

fix: handle Windows-style session paths when running on POSIX#50116

Open
RIPRODUCTIONS wants to merge 1 commit intoopenclaw:mainfrom
RIPRODUCTIONS:fix/windows-session-paths-on-posix
Open

fix: handle Windows-style session paths when running on POSIX#50116
RIPRODUCTIONS wants to merge 1 commit intoopenclaw:mainfrom
RIPRODUCTIONS:fix/windows-session-paths-on-posix

Conversation

@RIPRODUCTIONS
Copy link
Copy Markdown

Summary

  • When sessions.json is written by a Windows host but read inside a Linux Docker container, paths like C:\Users\...\abc.jsonl are treated as relative by path.isAbsolute (which only recognises POSIX absolutes)
  • This causes resolvePathWithinSessionsDir to concatenate the Windows path onto the container base, producing broken paths like /home/node/.openclaw/.../C:\Users\...\abc.jsonl
  • Adds isWindowsAbsoluteOnPosix() guard that detects drive-letter paths on POSIX and extracts the leaf filename, resolving it within the sessions directory

Test plan

  • Verify resolvePathWithinSessionsDir correctly extracts basename from C:\Users\foo\sessions\abc.jsonl on Linux
  • Verify C:/forward/slash/abc.jsonl variant also handled
  • Verify no behaviour change on native Windows (guard returns false when path.sep === "\\")
  • Verify normal POSIX relative paths still resolve correctly

🤖 Generated with Claude Code

When sessions.json is written by a Windows host but read inside a Linux
Docker container, paths like "C:\Users\...\abc.jsonl" are treated as
relative by path.isAbsolute (which only recognises POSIX absolutes).
This causes resolvePathWithinSessionsDir to concatenate the Windows path
onto the container base, producing broken paths like
"/home/node/.openclaw/.../C:\Users\...\abc.jsonl".

Add isWindowsAbsoluteOnPosix() guard that detects drive-letter paths on
POSIX and extracts the leaf filename, resolving it within the sessions
directory instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Copilot AI review requested due to automatic review settings March 19, 2026 01:36
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 19, 2026

Greptile Summary

This PR adds a cross-platform path guard to resolvePathWithinSessionsDir so that Windows-style absolute paths stored in sessions.json by a Windows host are handled correctly when read inside a Linux/Docker container. The approach — detect a drive-letter prefix on POSIX and extract only the leaf filename — is the right strategy given that path.isAbsolute is OS-specific and cannot recognise Windows paths on POSIX.

Issues found:

  • The early-return guard silently falls through for degenerate Windows paths that yield an empty basename (e.g. C:\ or C:\foo\), which can reproduce the broken concatenated path the PR is trying to fix. An explicit throw would be safer.
  • The ?? candidate fallback in extractBasename is dead code — split always returns at least one element, so the right-hand side of ?? is never reached. Using || instead would also cover the empty-string trailing-separator case.
  • Minor: the inner path.resolve(sessionsDir) in the early-return is redundant since path.resolve(a, b) already resolves a.

Confidence Score: 3/5

  • Safe to merge for the common case; degenerate Windows paths with no filename can still silently produce broken results.
  • The fix is correct for the documented scenario (well-formed Windows paths like C:\Users\foo\sessions\abc.jsonl). However, the silent fallthrough when basename extraction fails means edge-case inputs can bypass the guard and hit the old broken behaviour, and there are a couple of minor dead-code issues. The change is isolated to a single path-resolution helper, limiting blast radius.
  • src/config/sessions/paths.ts — specifically the fallthrough path in the new isWindowsAbsoluteOnPosix guard block.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/config/sessions/paths.ts
Line: 28

Comment:
**Unreachable `?? candidate` fallback**

`Array.prototype.split` always returns an array with at least one element (even `"".split(/[/\\]/)` yields `[""]`), so `parts[parts.length - 1]` can never be `undefined`. The `?? candidate` branch is dead code.

```suggestion
  return parts[parts.length - 1] || candidate;
```

Switching to `||` also covers the empty-string case — if the path ends with a separator (e.g. `C:\foo\`), `parts.at(-1)` is `""`, and `|| candidate` falls back to the original string, giving the caller a non-empty value to check against.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/config/sessions/paths.ts
Line: 211

Comment:
**Redundant double `path.resolve`**

`path.resolve(a, b)` already resolves `a` internally before joining, so the inner `path.resolve(sessionsDir)` is a no-op and just adds noise.

```suggestion
      return path.resolve(sessionsDir, basename);
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/config/sessions/paths.ts
Line: 208-213

Comment:
**Silent fallthrough on degenerate Windows paths**

If `isWindowsAbsoluteOnPosix` returns `true` but `basename` ends up empty (e.g. `C:\` or `C:\foo\`, where `split` yields a trailing `""`), the guard block is skipped and execution falls through to the normal POSIX logic. On POSIX `path.isAbsolute("C:\\foo\\")` returns `false`, so the Windows string is treated as a *relative* path and `path.resolve(realBase, "C:\\foo\\")` produces the broken concatenated path the PR is trying to prevent.

While a sessions entry stored without a filename is highly unlikely, the silent fallthrough means the guard's invariant can be violated. Consider throwing explicitly in this case:

```typescript
  if (isWindowsAbsoluteOnPosix(trimmed)) {
    const basename = extractBasename(trimmed);
    if (basename && basename !== "." && basename !== "..") {
      return path.resolve(sessionsDir, basename);
    }
    // Degenerate Windows path with no usable filename — cannot resolve safely.
    throw new Error("Session file path must be within sessions directory");
  }
```

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: "fix: handle Windows-..."

function extractBasename(candidate: string): string {
// Split on both separators to handle Windows paths on POSIX.
const parts = candidate.split(/[/\\]/);
return parts[parts.length - 1] ?? candidate;
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.

P2 Unreachable ?? candidate fallback

Array.prototype.split always returns an array with at least one element (even "".split(/[/\\]/) yields [""]), so parts[parts.length - 1] can never be undefined. The ?? candidate branch is dead code.

Suggested change
return parts[parts.length - 1] ?? candidate;
return parts[parts.length - 1] || candidate;

Switching to || also covers the empty-string case — if the path ends with a separator (e.g. C:\foo\), parts.at(-1) is "", and || candidate falls back to the original string, giving the caller a non-empty value to check against.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/config/sessions/paths.ts
Line: 28

Comment:
**Unreachable `?? candidate` fallback**

`Array.prototype.split` always returns an array with at least one element (even `"".split(/[/\\]/)` yields `[""]`), so `parts[parts.length - 1]` can never be `undefined`. The `?? candidate` branch is dead code.

```suggestion
  return parts[parts.length - 1] || candidate;
```

Switching to `||` also covers the empty-string case — if the path ends with a separator (e.g. `C:\foo\`), `parts.at(-1)` is `""`, and `|| candidate` falls back to the original string, giving the caller a non-empty value to check against.

How can I resolve this? If you propose a fix, please make it concise.

if (isWindowsAbsoluteOnPosix(trimmed)) {
const basename = extractBasename(trimmed);
if (basename && basename !== "." && basename !== "..") {
return path.resolve(path.resolve(sessionsDir), basename);
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.

P2 Redundant double path.resolve

path.resolve(a, b) already resolves a internally before joining, so the inner path.resolve(sessionsDir) is a no-op and just adds noise.

Suggested change
return path.resolve(path.resolve(sessionsDir), basename);
return path.resolve(sessionsDir, basename);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/config/sessions/paths.ts
Line: 211

Comment:
**Redundant double `path.resolve`**

`path.resolve(a, b)` already resolves `a` internally before joining, so the inner `path.resolve(sessionsDir)` is a no-op and just adds noise.

```suggestion
      return path.resolve(sessionsDir, basename);
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +208 to +213
if (isWindowsAbsoluteOnPosix(trimmed)) {
const basename = extractBasename(trimmed);
if (basename && basename !== "." && basename !== "..") {
return path.resolve(path.resolve(sessionsDir), basename);
}
}
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.

P2 Silent fallthrough on degenerate Windows paths

If isWindowsAbsoluteOnPosix returns true but basename ends up empty (e.g. C:\ or C:\foo\, where split yields a trailing ""), the guard block is skipped and execution falls through to the normal POSIX logic. On POSIX path.isAbsolute("C:\\foo\\") returns false, so the Windows string is treated as a relative path and path.resolve(realBase, "C:\\foo\\") produces the broken concatenated path the PR is trying to prevent.

While a sessions entry stored without a filename is highly unlikely, the silent fallthrough means the guard's invariant can be violated. Consider throwing explicitly in this case:

  if (isWindowsAbsoluteOnPosix(trimmed)) {
    const basename = extractBasename(trimmed);
    if (basename && basename !== "." && basename !== "..") {
      return path.resolve(sessionsDir, basename);
    }
    // Degenerate Windows path with no usable filename — cannot resolve safely.
    throw new Error("Session file path must be within sessions directory");
  }
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/config/sessions/paths.ts
Line: 208-213

Comment:
**Silent fallthrough on degenerate Windows paths**

If `isWindowsAbsoluteOnPosix` returns `true` but `basename` ends up empty (e.g. `C:\` or `C:\foo\`, where `split` yields a trailing `""`), the guard block is skipped and execution falls through to the normal POSIX logic. On POSIX `path.isAbsolute("C:\\foo\\")` returns `false`, so the Windows string is treated as a *relative* path and `path.resolve(realBase, "C:\\foo\\")` produces the broken concatenated path the PR is trying to prevent.

While a sessions entry stored without a filename is highly unlikely, the silent fallthrough means the guard's invariant can be violated. Consider throwing explicitly in this case:

```typescript
  if (isWindowsAbsoluteOnPosix(trimmed)) {
    const basename = extractBasename(trimmed);
    if (basename && basename !== "." && basename !== "..") {
      return path.resolve(sessionsDir, basename);
    }
    // Degenerate Windows path with no usable filename — cannot resolve safely.
    throw new Error("Session file path must be within sessions directory");
  }
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR improves cross-platform compatibility for session transcript paths by correctly handling Windows-style absolute paths when session metadata is read on POSIX (e.g., Linux containers).

Changes:

  • Adds POSIX-only detection for Windows drive-letter absolute paths.
  • Extracts the leaf filename from Windows-style paths and resolves it within the sessions directory to prevent malformed concatenations.

Comment on lines +26 to +30
// Split on both separators to handle Windows paths on POSIX.
const parts = candidate.split(/[/\\]/);
return parts[parts.length - 1] ?? candidate;
}

Comment on lines +208 to +213
if (isWindowsAbsoluteOnPosix(trimmed)) {
const basename = extractBasename(trimmed);
if (basename && basename !== "." && basename !== "..") {
return path.resolve(path.resolve(sessionsDir), basename);
}
}
Comment on lines +204 to +213
// Guard: if running on POSIX but the candidate is a Windows-style absolute
// path (e.g. stored in sessions.json by a Windows host and read inside a
// Docker container), extract the leaf filename to avoid creating paths like
// "/home/node/.openclaw/.../C:\Users\...".
if (isWindowsAbsoluteOnPosix(trimmed)) {
const basename = extractBasename(trimmed);
if (basename && basename !== "." && basename !== "..") {
return path.resolve(path.resolve(sessionsDir), basename);
}
}
// Already on Windows — path.isAbsolute handles it natively.
return false;
}
return /^[A-Za-z]:[/\\]/.test(candidate);
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c6075f5b08

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +208 to +211
if (isWindowsAbsoluteOnPosix(trimmed)) {
const basename = extractBasename(trimmed);
if (basename && basename !== "." && basename !== "..") {
return path.resolve(path.resolve(sessionsDir), basename);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve agent isolation when rewriting Windows session paths

When a POSIX host reads a Windows-authored sessionFile whose basename already exists locally (for example C:\...\agents\bot2\sessions\sess-1.jsonl while resolving a bot1 session), this branch now rewrites it straight to <sessionsDir>/sess-1.jsonl. That is a new silent aliasing bug: before, the bad Windows path stayed isolated under a distinct broken name, but after this change it can append to the wrong live transcript and resolveAndPersistSessionFile() will persist that alias back into sessions.json. The Windows-path fix needs to preserve agent/root context instead of collapsing everything to the basename.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants