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
13 changes: 10 additions & 3 deletions internal/agent/qodercli.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,12 +280,19 @@ func (a *QoderCLIAgent) RunTurn(ctx context.Context, rt Runtime, opts ExecOption
// are read only inside the runtime via Exec (not os.Getenv / host os.Stat).
func findQoderSessionFile(ctx context.Context, rt Runtime) string {
return findAgentSessionJSONL(ctx, rt, agentSessionLookup{
envVar: "SKILL_UP_QODER_WSKEY",
rootTmpl: "$home/.qoder/projects/$SKILL_UP_QODER_WSKEY",
findExtra: `! -name "*-session.json"`,
envVar: "SKILL_UP_QODER_WSKEY",
rootTmpl: "$home/.qoder/projects/$SKILL_UP_QODER_WSKEY",
findExtra: `! -name "*-session.json"`,
workspaceKeyTransform: qoderWorkspaceKey,
})
}

// qoderWorkspaceKey mirrors Qoder's projects-directory encoding. Unlike
// Claude Code, Qoder replaces underscores as well as path separators.
func qoderWorkspaceKey(key string) string {
return strings.ReplaceAll(key, "_", "-")
}

// Install installs qoder CLI via official install script.
//
//nolint:dupl // each agent Install shares the same probe→merge→exec lifecycle; the deltas (probe const, default install cmd) are pulled out, leaving the orchestration intentionally similar.
Expand Down
11 changes: 10 additions & 1 deletion internal/agent/qodercli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ func TestFindQoderSessionFile_SelectsNewestByModTime(t *testing.T) {
if err == nil {
workspace = realPath
}
workspaceKey := strings.ReplaceAll(workspace, "/", "-")
workspaceKey := qoderWorkspaceKey(strings.ReplaceAll(workspace, "/", "-"))
projectDir := filepath.Join(tmpHome, ".qoder", "projects", workspaceKey)
if err := os.MkdirAll(projectDir, 0o755); err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -459,6 +459,15 @@ func TestFindQoderSessionFile_SelectsNewestByModTime(t *testing.T) {
}
}

func TestQoderWorkspaceKey(t *testing.T) {
t.Parallel()

got := qoderWorkspaceKey("-private-var-folders-b_-skill-up")
if got != "-private-var-folders-b--skill-up" {
t.Fatalf("qoderWorkspaceKey() = %q", got)
}
}

func TestFindQoderSessionFileNoProject(t *testing.T) {
tmpDir := t.TempDir()
t.Setenv("HOME", tmpDir)
Expand Down
6 changes: 6 additions & 0 deletions internal/agent/session_lookup.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ type agentSessionLookup struct {
// findExtra appends extra `find` predicates such as exclusion patterns.
// Empty string means no extra predicates.
findExtra string
// workspaceKeyTransform applies agent-specific path encoding after the
// shared runtime workspace key has been normalized.
workspaceKeyTransform func(string) string

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟠 Major: Make workspace-key encoding fully agent-owned

workspaceKeyTransform func(string) string only post-processes workspaceKeyForRuntime, but that shared result already embeds Claude-specific path semantics. As a result, the new abstraction is still incomplete:

  • Qoder replaces more than underscores. Existing Qoder project directories encode punctuation such as .codex with -, while this patch preserves the dot on POSIX.
  • Qwen uses the same shared lookup without a transform, although upstream sanitizeCwd replaces every non-alphanumeric character with - and lowercases Windows paths. An underscore-containing Qwen workspace therefore has the same silent session-lookup/token-accounting failure class.

Please make the lookup accept a complete agent-owned encoder, e.g. type workspaceKeyEncoder func(Runtime) string, and require Claude, Qoder, and Qwen to each provide their full key implementation. The shared layer should continue to own session discovery, Windows cygpath conversion, download, and parsing; shared path preparation may remain a helper.

Please include table-driven coverage for _, ., :, Windows slash styles and 8.3 paths, plus Qwen Windows casing, and exercise the complete Qoder/Qwen runtime cwd -> key -> session lookup path. A real Windows canary can be supplied by maintainers if the contributor has no Windows environment.

Qwen reference: https://github.com/QwenLM/qwen-code/blob/7edc16ba11e1c4c3892c4f2664bd61d48cbf3cde/packages/core/src/utils/paths.ts


Source: manual evidence review @ b8b7911, cross-checked against the prior Claude Windows session recovery and Qwen upstream storage code.

}

// findAgentSessionJSONL resolves the newest *.jsonl session file under the
Expand All @@ -97,6 +100,9 @@ func findAgentSessionJSONL(ctx context.Context, rt Runtime, lookup agentSessionL
if workspaceKey == "" {
return ""
}
if lookup.workspaceKeyTransform != nil {
workspaceKey = lookup.workspaceKeyTransform(workspaceKey)
}
logging.DebugContextf(ctx, "agent session lookup: workspace=%q key=%q", rt.Workspace(), workspaceKey)

script := buildSessionLookupScript(lookup)
Expand Down
Loading