-
Notifications
You must be signed in to change notification settings - Fork 11
fix(session-sync): back up Claude sessions from CLAUDE_CONFIG_DIR #423
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
85474e9
705ea01
31eed1a
d852ffa
2b179ad
815f239
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| --- | ||
| "@prover-coder-ai/docker-git-session-sync": patch | ||
| --- | ||
|
|
||
| Back up Claude Code sessions from the resolved `CLAUDE_CONFIG_DIR` (issue #422). | ||
|
|
||
| docker-git points Claude Code at a custom `CLAUDE_CONFIG_DIR` | ||
| (`~/.docker-git/.orch/auth/claude/<label>`), so Claude writes chat transcripts to | ||
| `$CLAUDE_CONFIG_DIR/projects` instead of `~/.claude/projects`. The session backup | ||
| only scanned the home-relative paths, so the `.claude` folder in the | ||
| `docker-git-sessions` backup repo stayed empty. | ||
|
|
||
| The backup now resolves each session root from its agent environment override | ||
| (`CLAUDE_CONFIG_DIR` for Claude, `CODEX_HOME` for Codex) and falls back to the | ||
| home-relative directory when the override is unset, keeping the logical | ||
| `.claude/projects` / `.codex/sessions` names stable in the backup repo. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,7 +16,51 @@ export const backupDefaultBranch = "main" | |
| export const chunkManifestSuffix = ".chunks.json" | ||
| export const maxRepoFileSize = 20 * 1000 * 1000 | ||
| export const maxPushBatchBytes = 50 * 1000 * 1000 | ||
| export const sessionDirNames: ReadonlyArray<string> = [".codex/sessions", ".claude/projects"] | ||
| // CHANGE: Resolve session roots from agent env overrides (CLAUDE_CONFIG_DIR / CODEX_HOME). | ||
| // WHY: docker-git points Claude Code at a custom CLAUDE_CONFIG_DIR, so chat transcripts land in | ||
| // "$CLAUDE_CONFIG_DIR/projects" rather than "~/.claude/projects". The backup only scanned the | ||
| // home-relative paths, so the Claude folder in the backup repo stayed empty (issue #422). | ||
| // QUOTE(ТЗ): "Почему сессии от claude code не созраняются ... Папка claude вообще пустая" | ||
| // REF: issue-422 | ||
| // SOURCE: n/a | ||
| // FORMAT THEOREM: ∀spec,home,env: candidatePaths(spec) prefers env override base then home base. | ||
| // PURITY: CORE | ||
| // EFFECT: none | ||
| // INVARIANT: logical session name is stable regardless of which physical base is used. | ||
| // COMPLEXITY: O(1) | ||
| export interface SessionRootSpec { | ||
| // Logical name used inside the backup repo (e.g. ".claude/projects"). | ||
| readonly name: string | ||
| // Environment variable that overrides the base directory, when set and non-empty. | ||
| readonly envVar: string | null | ||
| // Sub-directory holding chat transcripts within the base directory. | ||
| readonly subDir: string | ||
| // Base directory relative to the user home when the env override is absent. | ||
| readonly homeBase: string | ||
| } | ||
|
|
||
| export const sessionRootSpecs: ReadonlyArray<SessionRootSpec> = [ | ||
| { name: ".codex/sessions", envVar: "CODEX_HOME", subDir: "sessions", homeBase: ".codex" }, | ||
| { name: ".claude/projects", envVar: "CLAUDE_CONFIG_DIR", subDir: "projects", homeBase: ".claude" } | ||
| ] | ||
|
|
||
| export const sessionDirNames: ReadonlyArray<string> = sessionRootSpecs.map((spec) => spec.name) | ||
|
|
||
| export const sessionRootCandidatePaths = ( | ||
| spec: SessionRootSpec, | ||
| homeDir: string, | ||
| env: Readonly<Record<string, string | undefined>> | ||
| ): ReadonlyArray<string> => { | ||
| const homePath = path.join(homeDir, spec.homeBase, spec.subDir) | ||
| const override = spec.envVar === null ? undefined : env[spec.envVar] | ||
| const trimmed = typeof override === "string" ? override.trim() : "" | ||
| if (trimmed.length === 0) { | ||
| return [homePath] | ||
| } | ||
| const overridePath = path.join(trimmed, spec.subDir) | ||
| return overridePath === homePath ? [homePath] : [overridePath, homePath] | ||
| } | ||
|
Comment on lines
+19
to
+62
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win Нет формальной TSDoc-документации для нового публичного API. Функциональный комментарий ( As per coding guidelines: "Provide comprehensive TSDoc comments with mathematical notation: 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
|
|
||
| export const sessionWalkIgnoreDirNames: ReadonlySet<string> = new Set([".git", "node_modules", "tmp"]) | ||
| export const githubEnvKeys: ReadonlyArray<string> = ["GITHUB_TOKEN", "GH_TOKEN"] | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,8 @@ import { | |
| formatTokenReduction, | ||
| isChatTranscriptPath, | ||
| maxRepoFileSize, | ||
| sessionRootCandidatePaths, | ||
| sessionRootSpecs, | ||
| shouldIgnoreSessionPath, | ||
| summarizeTokenReduction | ||
| } from "../src/core.js" | ||
|
|
@@ -146,6 +148,61 @@ describe("session path filtering", () => { | |
| }) | ||
| }) | ||
|
|
||
| describe("session root resolution", () => { | ||
| const codexSpec = sessionRootSpecs.find((spec) => spec.name === ".codex/sessions") | ||
| const claudeSpec = sessionRootSpecs.find((spec) => spec.name === ".claude/projects") | ||
|
|
||
| it("resolves Claude session roots from CLAUDE_CONFIG_DIR (issue #422)", () => { | ||
| expect(claudeSpec).toBeDefined() | ||
| if (claudeSpec === undefined) { | ||
| return | ||
| } | ||
| const configDir = path.join(tmpDir, ".docker-git", ".orch", "auth", "claude", "default") | ||
| const candidates = sessionRootCandidatePaths(claudeSpec, "/home/dev", { | ||
| CLAUDE_CONFIG_DIR: configDir | ||
| }) | ||
|
|
||
| // The env override wins, but the home-relative path stays as a fallback. | ||
| expect(candidates).toEqual([ | ||
| path.join(configDir, "projects"), | ||
| path.join("/home/dev", ".claude", "projects") | ||
| ]) | ||
| }) | ||
|
|
||
| it("falls back to the home directory when the env override is empty", () => { | ||
| expect(codexSpec).toBeDefined() | ||
| if (codexSpec === undefined || claudeSpec === undefined) { | ||
| return | ||
| } | ||
| expect(sessionRootCandidatePaths(codexSpec, "/home/dev", {})).toEqual([ | ||
| path.join("/home/dev", ".codex", "sessions") | ||
| ]) | ||
| expect(sessionRootCandidatePaths(claudeSpec, "/home/dev", { CLAUDE_CONFIG_DIR: " " })).toEqual([ | ||
| path.join("/home/dev", ".claude", "projects") | ||
| ]) | ||
| }) | ||
|
|
||
| it("resolves Codex session roots from CODEX_HOME", () => { | ||
| if (codexSpec === undefined) { | ||
| return | ||
| } | ||
| const codexHome = path.join(tmpDir, "codex-home") | ||
| expect(sessionRootCandidatePaths(codexSpec, "/home/dev", { CODEX_HOME: codexHome })).toEqual([ | ||
| path.join(codexHome, "sessions"), | ||
| path.join("/home/dev", ".codex", "sessions") | ||
| ]) | ||
| }) | ||
|
|
||
| it("collapses to a single candidate when the override matches the home path", () => { | ||
| if (claudeSpec === undefined) { | ||
| return | ||
| } | ||
| expect( | ||
| sessionRootCandidatePaths(claudeSpec, "/home/dev", { CLAUDE_CONFIG_DIR: "/home/dev/.claude" }) | ||
| ).toEqual([path.join("/home/dev", ".claude", "projects")]) | ||
| }) | ||
| }) | ||
|
Comment on lines
+151
to
+204
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win Отсутствует property-based тест для задокументированного инварианта Новые тесты используют только фиксированные примеры, хотя в As per coding guidelines: "Write property-based tests using fast-check (fc.property) to verify mathematical invariants". 🧪 Пример property-based тестаit("candidate paths prefer the env override then fall back to home (invariant)", () => {
fc.assert(
fc.property(pathPartArbitrary, pathPartArbitrary, fc.option(pathPartArbitrary, { nil: undefined }),
(home, homeBase, override) => {
const spec: SessionRootSpec = { name: "x", envVar: "X_HOME", subDir: "sub", homeBase }
const env = override === undefined ? {} : { X_HOME: override }
const homePath = path.join(home, homeBase, "sub")
const result = sessionRootCandidatePaths(spec, home, env)
const trimmed = override?.trim() ?? ""
if (trimmed.length === 0) return result.length === 1 && result[0] === homePath
const overridePath = path.join(trimmed, "sub")
return overridePath === homePath
? result.length === 1 && result[0] === homePath
: result.length === 2 && result[0] === overridePath && result[1] === homePath
}
)
)
})🤖 Prompt for AI AgentsSource: Coding guidelines |
||
|
|
||
| describe("snapshot refs", () => { | ||
| it("uses stable current refs for PR and branch snapshots", () => { | ||
| expect(buildSnapshotRef("org/repo", 230, "issue-230")).toBe("org/repo/pr-230/current") | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff
Прямой доступ к
process.env/fsв обход Effect/Layer.Гайдлайн требует, чтобы окружение и файловая система были доступны только через Effect-based интерфейсы/Layer, а не напрямую.
getAllowedSessionRootsпродолжает (как и раньше) обращаться кprocess.envиfs.existsSyncнапрямую. Полный переход на Effect/Layer для этого модуля — это переработка архитектуры всего файла, а не локальный фикс, поэтому оцениваю как затратный при умеренной пользе именно в контексте этого PR.As per coding guidelines: "All external services (database, HTTP, environment) must be accessed through Effect-based interfaces and Layer-based dependency injection; never call external APIs directly".
🤖 Prompt for AI Agents
Source: Coding guidelines