From dba161b4239b47ce38a06b6ec55244eae4b4f8cd Mon Sep 17 00:00:00 2001 From: Marius van Niekerk Date: Wed, 24 Jun 2026 21:34:00 -0400 Subject: [PATCH] feat(parser): migrate qwen provider Qwen uses a nested project/chats JSONL source shape, so it is a good next provider facade slice after the shallow and directory JSONL migrations. Moving it behind a concrete provider keeps discovery, lookup, fingerprinting, and parse output explicit without introducing another source framework. Legacy discovery accepts any one-level .jsonl file under chats while raw-session lookup still validates the requested ID before matching filename-derived IDs. The provider keeps that asymmetry, symlinked project directory and file behavior, project hints, and existing parser normalization intact. Validation: go fmt ./...; go test -tags "fts5" ./internal/parser -run TestQwenProvider -count=1; go test -tags "fts5" ./internal/parser -count=1; make test-short; go vet ./...; git diff --check test(parser): opt qwen into provider shadow Qwen now has a concrete facade provider on this branch, so its migration mode should enter shadow comparison instead of remaining an additive legacy-only provider. Lower provider opt-ins stay inherited and later branches remain responsible for their own concrete providers. Validation: go test -tags "fts5" ./internal/parser -run TestProviderMigrationModes -count=1; go test -tags "fts5" ./internal/parser -count=1; go vet ./...; git diff --check test(sync): compare qwen shadow parity Qwen is shadow-compared on this branch, so add the source-level migration proof that provider observation matches ParseQwenSession for its nested project/chats layout. This keeps reviewers focused on behavioral parity while later branches continue migrating their own provider shapes. Validation: go test -tags "fts5" ./internal/parser ./internal/sync -run 'TestObserveProviderSourceMatchesQwenLegacyParser|TestQwenProvider|TestParseQwen' -count=1; go fmt ./...; go vet ./...; git diff --check; go test -tags "fts5" ./internal/parser ./internal/sync -count=1; ./custom-gcl run --config .golangci.nilaway.yml ./internal/parser/... ./internal/sync/... refactor(parser): fold qwen into provider Qwen already had a concrete provider, but the branch still kept exported legacy parser/source functions and legacy sync dispatch. That left the migration additive instead of making the provider shape authoritative.\n\nMove Qwen parsing behind the provider method, remove the registry callbacks and sync processor/classifier, and replace the shadow comparison with provider API coverage plus a guard that the old entrypoints stay gone.\n\nValidation: go test -tags "fts5" ./internal/parser -run 'TestQwen|TestParseQwenSession' -count=1 -v; go test -tags "fts5" ./internal/sync -run 'TestEngine_ClassifyPathsQwenSession|TestProviderMigration|TestObserveProvider|TestSyncSingle.*Qwen|TestQwen' -count=1 -v; go test -tags "fts5" ./internal/parser ./internal/sync ./cmd/agentsview -count=1; go vet ./...; git diff --check fix(parser): thread ctx through qwen source lookups --- internal/parser/provider.go | 2 + internal/parser/provider_migration.go | 2 +- internal/parser/qwen.go | 76 +------------ internal/parser/qwen_provider.go | 94 ++++++++++++++++ internal/parser/qwen_provider_test.go | 150 ++++++++++++++++++++++++++ internal/parser/qwen_test.go | 70 +++++++++--- internal/parser/types.go | 4 +- internal/sync/engine.go | 59 ---------- 8 files changed, 306 insertions(+), 151 deletions(-) create mode 100644 internal/parser/qwen_provider.go create mode 100644 internal/parser/qwen_provider_test.go diff --git a/internal/parser/provider.go b/internal/parser/provider.go index ed3975317..f8413a11e 100644 --- a/internal/parser/provider.go +++ b/internal/parser/provider.go @@ -359,6 +359,8 @@ func providerFactoryForDef(def AgentDef) ProviderFactory { return newGptmeProviderFactory(def) case AgentOMP, AgentPi: return newPiProviderFactory(def) + case AgentQwen: + return newQwenProviderFactory(def) case AgentZencoder: return newZencoderProviderFactory(def) default: diff --git a/internal/parser/provider_migration.go b/internal/parser/provider_migration.go index eb7ea76d8..27356b66e 100644 --- a/internal/parser/provider_migration.go +++ b/internal/parser/provider_migration.go @@ -33,7 +33,7 @@ var providerMigrationModes = map[AgentType]ProviderMigrationMode{ AgentVSCodeCopilot: ProviderMigrationLegacyOnly, AgentVSCopilot: ProviderMigrationLegacyOnly, AgentPi: ProviderMigrationProviderAuthoritative, - AgentQwen: ProviderMigrationLegacyOnly, + AgentQwen: ProviderMigrationProviderAuthoritative, AgentCommandCode: ProviderMigrationProviderAuthoritative, AgentDeepSeekTUI: ProviderMigrationProviderAuthoritative, AgentOpenClaw: ProviderMigrationLegacyOnly, diff --git a/internal/parser/qwen.go b/internal/parser/qwen.go index 46f39bcc6..5fac715e0 100644 --- a/internal/parser/qwen.go +++ b/internal/parser/qwen.go @@ -5,85 +5,13 @@ import ( "fmt" "os" "path/filepath" - "sort" "strings" "time" "github.com/tidwall/gjson" ) -// DiscoverQwenSessions finds Qwen Code chat transcripts under the -// projects root. The directory structure is: -// //chats/.jsonl -func DiscoverQwenSessions(projectsDir string) []DiscoveredFile { - if projectsDir == "" { - return nil - } - - projectEntries, err := os.ReadDir(projectsDir) - if err != nil { - return nil - } - - var files []DiscoveredFile - for _, entry := range projectEntries { - if !isDirOrSymlink(entry, projectsDir) { - continue - } - - projectDir := filepath.Join(projectsDir, entry.Name()) - chatsDir := filepath.Join(projectDir, "chats") - chatEntries, err := os.ReadDir(chatsDir) - if err != nil { - continue - } - - project := GetProjectName(entry.Name()) - for _, chat := range chatEntries { - if chat.IsDir() || !strings.HasSuffix(chat.Name(), ".jsonl") { - continue - } - files = append(files, DiscoveredFile{ - Path: filepath.Join(chatsDir, chat.Name()), - Project: project, - Agent: AgentQwen, - }) - } - } - - sort.Slice(files, func(i, j int) bool { - return files[i].Path < files[j].Path - }) - return files -} - -// FindQwenSourceFile locates a Qwen session file by its raw session -// ID (without the "qwen:" prefix). -func FindQwenSourceFile(projectsDir, rawID string) string { - if projectsDir == "" || !IsValidSessionID(rawID) { - return "" - } - - projectEntries, err := os.ReadDir(projectsDir) - if err != nil { - return "" - } - for _, entry := range projectEntries { - if !isDirOrSymlink(entry, projectsDir) { - continue - } - - candidate := filepath.Join( - projectsDir, entry.Name(), "chats", rawID+".jsonl", - ) - if _, err := os.Stat(candidate); err == nil { - return candidate - } - } - return "" -} - -// ParseQwenSession parses a Qwen Code JSONL chat transcript. +// parseSession parses a Qwen Code JSONL chat transcript. // // Qwen emits one `type=assistant` line per model output, including // every tool-call iteration in a multi-step turn. Each iteration's @@ -96,7 +24,7 @@ func FindQwenSourceFile(projectsDir, rawID string) string { // aggregating their thinking text and token usage. A trailing run of // tool-call-only entries with no text follow-up is emitted as a single // coalesced assistant message so the data isn't lost. -func ParseQwenSession( +func parseQwenSession( path, project, machine string, ) (*ParsedSession, []ParsedMessage, error) { info, err := os.Stat(path) diff --git a/internal/parser/qwen_provider.go b/internal/parser/qwen_provider.go new file mode 100644 index 000000000..00d06714f --- /dev/null +++ b/internal/parser/qwen_provider.go @@ -0,0 +1,94 @@ +package parser + +import ( + "context" + "path/filepath" + "strings" +) + +// Qwen stores each chat as a JSONL transcript under a per-project +// directory. It is a directory-of-files provider: discovery, watching, +// change classification, lookup, and fingerprinting come from +// JSONLSourceSet, and the ParseFile option makes that source set a full +// SourceSet so it rides the generic factory. +func newQwenProviderFactory(def AgentDef) ProviderFactory { + return newSourceSetFactory( + def, + qwenProviderCapabilities(), + func(cfg ProviderConfig) SourceSet { return newQwenSourceSet(cfg.Roots) }, + ) +} + +func newQwenSourceSet(roots []string) JSONLSourceSet { + return newJSONLSourceSet(AgentQwen, roots, + withRecursive(), + withSymlinkFollowing(), + withIncludePath(isQwenSourcePath), + withProjectHint(qwenProjectHintFromPath), + withSessionIDFromPath(qwenSessionIDFromPath), + withParseFile(qwenParseFile), + ) +} + +func qwenParseFile( + _ context.Context, path string, req ParseRequest, +) ([]ParseResult, []string, error) { + sess, msgs, err := parseQwenSession(path, req.Source.ProjectHint, req.Machine) + if err != nil { + return nil, nil, err + } + if sess == nil { + return nil, nil, nil + } + if req.Fingerprint.Hash != "" { + sess.File.Hash = req.Fingerprint.Hash + } + return []ParseResult{{Session: *sess, Messages: msgs}}, nil, nil +} + +func isQwenSourcePath(root, path string) bool { + rel, err := filepath.Rel(root, path) + if err != nil { + return false + } + parts := strings.Split(rel, string(filepath.Separator)) + return len(parts) == 3 && + parts[0] != "" && parts[0] != "." && parts[0] != ".." && + parts[1] == "chats" && + parts[2] != "" && parts[2] != "." && parts[2] != ".." && + strings.HasSuffix(parts[2], ".jsonl") +} + +func qwenProjectHintFromPath(root, path string) string { + rel, err := filepath.Rel(root, path) + if err != nil { + return "" + } + parts := strings.Split(rel, string(filepath.Separator)) + if len(parts) != 3 { + return "" + } + return GetProjectName(parts[0]) +} + +func qwenSessionIDFromPath(root, path string) string { + if !isQwenSourcePath(root, path) { + return "" + } + return strings.TrimSuffix(filepath.Base(path), ".jsonl") +} + +func qwenProviderCapabilities() Capabilities { + return Capabilities{ + Source: jsonlFileProviderSourceCapabilities(), + Content: ContentCapabilities{ + FirstMessage: CapabilitySupported, + Cwd: CapabilitySupported, + Thinking: CapabilitySupported, + ToolCalls: CapabilitySupported, + ToolResults: CapabilitySupported, + PerMessageTokenUsage: CapabilitySupported, + Model: CapabilitySupported, + }, + } +} diff --git a/internal/parser/qwen_provider_test.go b/internal/parser/qwen_provider_test.go new file mode 100644 index 000000000..0e9608eb4 --- /dev/null +++ b/internal/parser/qwen_provider_test.go @@ -0,0 +1,150 @@ +package parser + +import ( + "context" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestQwenProviderFactoryReplacesLegacyAdapter(t *testing.T) { + factory, ok := ProviderFactoryByType(AgentQwen) + require.True(t, ok) + require.NotNil(t, factory) + + provider, ok := NewProvider(AgentQwen, ProviderConfig{ + Roots: []string{t.TempDir()}, + Machine: "devbox", + }) + require.True(t, ok) + require.NotNil(t, provider) +} + +func TestQwenProviderSourceMethods(t *testing.T) { + root := t.TempDir() + projectDir := filepath.Join(root, "-Users-alice-code-sample-project") + sourcePath := filepath.Join(projectDir, "chats", "session-123.jsonl") + nonIDPath := filepath.Join(projectDir, "chats", "2025.01.01.jsonl") + writeSourceFile(t, sourcePath, qwenProviderFixture("session-123")) + writeSourceFile(t, nonIDPath, qwenProviderFixture("header-session-id")) + writeSourceFile(t, filepath.Join(projectDir, "notes", "skip.jsonl"), "{}\n") + writeSourceFile(t, filepath.Join(root, "root-session.jsonl"), "{}\n") + writeSourceFile(t, filepath.Join(projectDir, "chats", "nested", "deep.jsonl"), "{}\n") + + provider, ok := NewProvider(AgentQwen, ProviderConfig{ + Roots: []string{root}, + Machine: "devbox", + }) + require.True(t, ok) + + discovered, err := provider.Discover(context.Background()) + require.NoError(t, err) + require.Len(t, discovered, 2) + assert.Equal(t, []string{nonIDPath, sourcePath}, sourceDisplayPaths(discovered)) + assert.Equal(t, []string{"sample_project", "sample_project"}, sourceProjects(discovered)) + + plan, err := provider.WatchPlan(context.Background()) + require.NoError(t, err) + require.Len(t, plan.Roots, 1) + assert.Equal(t, root, plan.Roots[0].Path) + assert.True(t, plan.Roots[0].Recursive) + assert.Equal(t, []string{"*.jsonl"}, plan.Roots[0].IncludeGlobs) + + found, ok, err := provider.FindSource(context.Background(), FindSourceRequest{ + FullSessionID: "host~qwen:session-123", + }) + require.NoError(t, err) + require.True(t, ok) + assert.Equal(t, sourcePath, found.DisplayPath) + + _, ok, err = provider.FindSource(context.Background(), FindSourceRequest{ + RawSessionID: "2025.01.01", + }) + require.NoError(t, err) + assert.False(t, ok) + + found, ok, err = provider.FindSource(context.Background(), FindSourceRequest{ + StoredFilePath: nonIDPath, + }) + require.NoError(t, err) + require.True(t, ok) + assert.Equal(t, nonIDPath, found.DisplayPath) + + require.NoError(t, os.Remove(sourcePath)) + changed, err := provider.SourcesForChangedPath( + context.Background(), + ChangedPathRequest{Path: sourcePath, EventKind: "remove", WatchRoot: root}, + ) + require.NoError(t, err) + require.Len(t, changed, 1) + assert.Equal(t, sourcePath, changed[0].DisplayPath) +} + +func TestQwenProviderDiscoversSymlinkedProjectDirectory(t *testing.T) { + root := t.TempDir() + targetDir := t.TempDir() + sourcePath := filepath.Join(root, "-Users-alice-code-sample-project", "chats", "session-123.jsonl") + targetPath := filepath.Join(targetDir, "chats", "session-123.jsonl") + writeSourceFile(t, targetPath, qwenProviderFixture("session-123")) + if err := os.Symlink(targetDir, filepath.Join(root, "-Users-alice-code-sample-project")); err != nil { + t.Skipf("symlink not supported: %v", err) + } + + provider, ok := NewProvider(AgentQwen, ProviderConfig{ + Roots: []string{root}, + Machine: "devbox", + }) + require.True(t, ok) + + discovered, err := provider.Discover(context.Background()) + require.NoError(t, err) + require.Len(t, discovered, 1) + assert.Equal(t, sourcePath, discovered[0].DisplayPath) + + found, ok, err := provider.FindSource(context.Background(), FindSourceRequest{ + FullSessionID: "host~qwen:session-123", + }) + require.NoError(t, err) + require.True(t, ok) + assert.Equal(t, sourcePath, found.DisplayPath) +} + +func TestQwenProviderParse(t *testing.T) { + root := t.TempDir() + sourcePath := filepath.Join(root, "-Users-alice-code-sample-project", "chats", "session-123.jsonl") + writeSourceFile(t, sourcePath, qwenProviderFixture("session-123")) + + provider, ok := NewProvider(AgentQwen, ProviderConfig{ + Roots: []string{root}, + Machine: "devbox", + }) + require.True(t, ok) + sources, err := provider.Discover(context.Background()) + require.NoError(t, err) + require.Len(t, sources, 1) + + outcome, err := provider.Parse(context.Background(), ParseRequest{ + Source: sources[0], + Fingerprint: SourceFingerprint{Key: sourcePath, Hash: "abc123"}, + }) + require.NoError(t, err) + require.True(t, outcome.ResultSetComplete) + require.Len(t, outcome.Results, 1) + assert.Equal(t, DataVersionCurrent, outcome.Results[0].DataVersion) + assert.Equal(t, "qwen:session-123", outcome.Results[0].Result.Session.ID) + assert.Equal(t, "sample_project", outcome.Results[0].Result.Session.Project) + assert.Equal(t, "devbox", outcome.Results[0].Result.Session.Machine) + assert.Equal(t, "abc123", outcome.Results[0].Result.Session.File.Hash) + assert.Len(t, outcome.Results[0].Result.Messages, 2) +} + +func qwenProviderFixture(sessionID string) string { + return strings.Join([]string{ + `{"uuid":"u1","sessionId":"` + sessionID + `","timestamp":"2026-05-05T11:08:38.572Z","type":"user","cwd":"/Users/alice/code/sample-project","message":{"role":"user","parts":[{"text":"Calculate .089 * 7.85788"}]}}`, + `{"uuid":"u2","sessionId":"` + sessionID + `","timestamp":"2026-05-05T11:08:46.529Z","type":"assistant","cwd":"/Users/alice/code/sample-project","model":"qwen","message":{"role":"model","parts":[{"text":"The user wants multiplication.","thought":true},{"text":"0.089 times 7.85788 is 0.69935132"}]},"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":20,"cachedContentTokenCount":5}}`, + }, "\n") +} diff --git a/internal/parser/qwen_test.go b/internal/parser/qwen_test.go index 5526bf79c..21cab122f 100644 --- a/internal/parser/qwen_test.go +++ b/internal/parser/qwen_test.go @@ -1,6 +1,7 @@ package parser import ( + "context" "os" "path/filepath" "testing" @@ -10,6 +11,47 @@ import ( "github.com/tidwall/gjson" ) +func parseQwenTestSession( + t testing.TB, + path, project, machine string, +) (*ParsedSession, []ParsedMessage, error) { + t.Helper() + return parseQwenSession(path, project, machine) +} + +func discoverQwenTestSessions(t testing.TB, root string) []DiscoveredFile { + t.Helper() + provider, ok := NewProvider(AgentQwen, ProviderConfig{Roots: []string{root}}) + require.True(t, ok) + sources, err := provider.Discover(context.Background()) + require.NoError(t, err) + + files := make([]DiscoveredFile, 0, len(sources)) + for _, source := range sources { + files = append(files, DiscoveredFile{ + Path: source.DisplayPath, + Project: source.ProjectHint, + Agent: source.Provider, + }) + } + return files +} + +func findQwenTestSourceFile(t testing.TB, root, rawID string) string { + t.Helper() + provider, ok := NewProvider(AgentQwen, ProviderConfig{Roots: []string{root}}) + require.True(t, ok) + source, found, err := provider.FindSource( + context.Background(), + FindSourceRequest{RawSessionID: rawID}, + ) + require.NoError(t, err) + if !found { + return "" + } + return source.DisplayPath +} + func TestParseQwenSession(t *testing.T) { t.Parallel() @@ -19,7 +61,7 @@ func TestParseQwenSession(t *testing.T) { path := createTestFile(t, "qwen-session.jsonl", content) - sess, msgs, err := ParseQwenSession(path, "", "local") + sess, msgs, err := parseQwenTestSession(t, path, "", "local") require.NoError(t, err) require.NotNil(t, sess) require.Len(t, msgs, 2) @@ -74,7 +116,7 @@ func TestParseQwenSession_CoalescesToolCallOnlyAssistants(t *testing.T) { path := createTestFile(t, "coalesce.jsonl", content) - sess, msgs, err := ParseQwenSession(path, "", "local") + sess, msgs, err := parseQwenTestSession(t, path, "", "local") require.NoError(t, err) require.Len(t, msgs, 2, "expected user + single coalesced assistant turn") require.Equal(t, 2, sess.MessageCount) @@ -141,7 +183,7 @@ func TestParseQwenSession_TrailingToolCallOnlyAssistants(t *testing.T) { path := createTestFile(t, "trail.jsonl", content) - sess, msgs, err := ParseQwenSession(path, "", "local") + sess, msgs, err := parseQwenTestSession(t, path, "", "local") require.NoError(t, err) require.Len(t, msgs, 2, "trailing tool-call run should coalesce into one assistant message") require.Equal(t, 2, sess.MessageCount) @@ -188,7 +230,7 @@ func TestParseQwenSession_ToolUseRoundTrip(t *testing.T) { path := createTestFile(t, "tools.jsonl", content) - sess, msgs, err := ParseQwenSession(path, "", "local") + sess, msgs, err := parseQwenTestSession(t, path, "", "local") require.NoError(t, err) require.Len(t, msgs, 2, "tool-result user entry should fold into the assistant turn") @@ -235,7 +277,7 @@ func TestParseQwenSession_TextWithFunctionCallCoalesces(t *testing.T) { path := createTestFile(t, "interleaved.jsonl", content) - sess, msgs, err := ParseQwenSession(path, "", "local") + sess, msgs, err := parseQwenTestSession(t, path, "", "local") require.NoError(t, err) require.Len(t, msgs, 2, "interleaved text+functionCall must not inflate MessageCount") @@ -269,7 +311,7 @@ func TestParseQwenSession_AbortedNoAssistantResponse(t *testing.T) { path := createTestFile(t, "abort.jsonl", content) - sess, msgs, err := ParseQwenSession(path, "", "local") + sess, msgs, err := parseQwenTestSession(t, path, "", "local") require.NoError(t, err) require.Len(t, msgs, 1) assert.Equal(t, 1, sess.MessageCount) @@ -291,7 +333,7 @@ func TestParseQwenSession_ShortClean(t *testing.T) { path := createTestFile(t, "short.jsonl", content) - sess, msgs, err := ParseQwenSession(path, "", "local") + sess, msgs, err := parseQwenTestSession(t, path, "", "local") require.NoError(t, err) require.Len(t, msgs, 2) assert.Equal(t, 2, sess.MessageCount) @@ -300,7 +342,7 @@ func TestParseQwenSession_ShortClean(t *testing.T) { assert.False(t, msgs[1].HasThinking) } -func TestDiscoverQwenSessions(t *testing.T) { +func TestQwenProviderDiscoverSessions(t *testing.T) { t.Parallel() root := t.TempDir() @@ -311,7 +353,7 @@ func TestDiscoverQwenSessions(t *testing.T) { require.NoError(t, os.WriteFile(filepath.Join(chatsDir, "b.jsonl"), []byte("{}\n"), 0o644)) require.NoError(t, os.WriteFile(filepath.Join(chatsDir, "notes.txt"), []byte("ignore"), 0o644)) - files := DiscoverQwenSessions(root) + files := discoverQwenTestSessions(t, root) require.Len(t, files, 2) assert.Equal(t, AgentQwen, files[0].Agent) assert.Equal(t, "qwen", files[0].Project) @@ -319,7 +361,7 @@ func TestDiscoverQwenSessions(t *testing.T) { assert.Equal(t, "qwen", files[1].Project) } -func TestFindQwenSourceFile(t *testing.T) { +func TestQwenProviderFindSourceFile(t *testing.T) { t.Parallel() root := t.TempDir() @@ -331,8 +373,8 @@ func TestFindQwenSourceFile(t *testing.T) { want := filepath.Join(chatsDir, sessionID+".jsonl") require.NoError(t, os.WriteFile(want, []byte("{}\n"), 0o644)) - assert.Equal(t, want, FindQwenSourceFile(root, sessionID)) - assert.Empty(t, FindQwenSourceFile(root, "not-a-session-id")) - assert.Empty(t, FindQwenSourceFile(root, "b0a4eadd-cb99-4165-94d9-64cad5a66d99")) - assert.Empty(t, FindQwenSourceFile("", sessionID)) + assert.Equal(t, want, findQwenTestSourceFile(t, root, sessionID)) + assert.Empty(t, findQwenTestSourceFile(t, root, "not-a-session-id")) + assert.Empty(t, findQwenTestSourceFile(t, root, "b0a4eadd-cb99-4165-94d9-64cad5a66d99")) + assert.Empty(t, findQwenTestSourceFile(t, "", sessionID)) } diff --git a/internal/parser/types.go b/internal/parser/types.go index d80571c4e..7f83e38b9 100644 --- a/internal/parser/types.go +++ b/internal/parser/types.go @@ -332,9 +332,7 @@ var Registry = []AgentDef{ // Sessions live under //chats/.jsonl, // so the projects root must be watched recursively — pinning the // watch to a "chats" subdir of the root catches no events. - FileBased: true, - DiscoverFunc: DiscoverQwenSessions, - FindSourceFunc: FindQwenSourceFile, + FileBased: true, }, { Type: AgentCommandCode, diff --git a/internal/sync/engine.go b/internal/sync/engine.go index ad9038e89..27dfce404 100644 --- a/internal/sync/engine.go +++ b/internal/sync/engine.go @@ -1346,28 +1346,6 @@ func (e *Engine) classifyOnePath( return df, true } - // Qwen: //chats/.jsonl - for _, qwenDir := range e.agentDirs[parser.AgentQwen] { - if qwenDir == "" { - continue - } - if rel, ok := isUnder(qwenDir, path); ok { - parts := strings.Split(rel, sep) - if len(parts) != 3 || parts[1] != "chats" { - continue - } - sessionID, ok := strings.CutSuffix(parts[2], ".jsonl") - if !ok || !parser.IsValidSessionID(sessionID) { - continue - } - return parser.DiscoveredFile{ - Path: path, - Project: parser.GetProjectName(parts[0]), - Agent: parser.AgentQwen, - }, true - } - } - if df, ok := e.classifyAiderPath(path); ok { return df, true } @@ -4504,8 +4482,6 @@ func (e *Engine) processFile( res = e.processVSCodeCopilot(file, info) case parser.AgentVSCopilot: res = e.processVisualStudioCopilot(file, info) - case parser.AgentQwen: - res = e.processQwen(file, info) case parser.AgentOpenClaw: res = e.processOpenClaw(file, info) case parser.AgentQClaw: @@ -7072,36 +7048,6 @@ func (e *Engine) processCursor( } } -func (e *Engine) processQwen( - file parser.DiscoveredFile, info os.FileInfo, -) processResult { - if e.shouldSkipByPath(file.Path, info) { - return processResult{skip: true} - } - - sess, msgs, err := parser.ParseQwenSession( - file.Path, file.Project, e.machine, - ) - if err != nil { - return processResult{err: err} - } - if sess == nil { - return processResult{} - } - - hash, err := ComputeFileHash(file.Path) - if err == nil { - sess.File.Hash = hash - } - - return processResult{ - results: []parser.ParseResult{{ - Session: *sess, - Messages: msgs, - }}, - } -} - func commandCodeEffectiveInfo(path string, info os.FileInfo) os.FileInfo { size := info.Size() mtime := info.ModTime().UnixNano() @@ -9521,11 +9467,6 @@ func (e *Engine) SyncSingleSessionContext( file.Project = sess.Project } } - case parser.AgentQwen: - // path is //chats/.jsonl - file.Project = parser.GetProjectName( - filepath.Base(filepath.Dir(filepath.Dir(path))), - ) case parser.AgentWorkBuddy: for _, workBuddyDir := range e.agentDirs[parser.AgentWorkBuddy] { rel, ok := isUnder(workBuddyDir, path)