Skip to content

Commit b90a86b

Browse files
authored
Merge pull request #974 from web3dev1337/fix/workspace-backward-compat
fix: restore legacy workspace data fallback
2 parents b50b8f8 + e45d0d3 commit b90a86b

4 files changed

Lines changed: 384 additions & 12 deletions

File tree

CODEBASE_DOCUMENTATION.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@ server/utils/processUtils.js - Shared spawn/env hardening helpers
8383
└─ Cross-platform behavior: non-Windows platforms pass through unchanged so Linux/macOS launch behavior stays stable
8484
server/utils/nodePtyCompat.js - Runtime compatibility shim for the bundled `node-pty` Windows ConPTY loader
8585
└─ Windows PTY guard: wraps the known-bad `node-pty@1.2.0-beta.12` `conpty.startProcess()` call in memory so packaged installs survive read-only app-resource layouts
86-
server/utils/pathUtils.js - Shared slash-normalization helpers for repo/worktree labels
87-
└─ Used by server-side workspace/conversation flows to keep Windows backslash paths compatible with Linux-style UI labels
86+
server/utils/pathUtils.js - Shared slash-normalization + data-directory compatibility helpers for repo/worktree labels
87+
└─ Legacy migration: renames `~/.orchestrator` when possible, otherwise merges richer legacy state into `~/.agent-workspace` with conflict backups before falling back to the old directory
8888
server/tokenCounter.js - Token usage tracking (if applicable)
8989
server/userSettingsService.js - User preferences and settings management
9090
server/sessionRecoveryService.js - Session recovery state persistence (CWD, agents, conversations)

server/index.js

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,18 @@ const os = require('os');
88
const crypto = require('crypto');
99
const winston = require('winston');
1010
const { augmentProcessEnv, getHiddenProcessOptions } = require('./utils/processUtils');
11-
const { migrateFromOrchestratorDir, bootstrapProjectsRoot } = require('./utils/pathUtils');
11+
const {
12+
migrateFromOrchestratorDir,
13+
mergeLegacyDataDir,
14+
bootstrapProjectsRoot,
15+
getAgentWorkspaceDir,
16+
getLegacyCompatibilityState
17+
} = require('./utils/pathUtils');
1218

1319
const migratedDataDir = migrateFromOrchestratorDir();
20+
const mergedLegacyDataDir = mergeLegacyDataDir();
21+
const legacyCompatibilityState = getLegacyCompatibilityState();
22+
const resolvedDataDir = getAgentWorkspaceDir();
1423
const projectsRootBootstrap = bootstrapProjectsRoot();
1524

1625
// Ensure log directory exists early (some services create file transports at require-time).
@@ -50,6 +59,24 @@ const logger = winston.createLogger({
5059
if (migratedDataDir) {
5160
logger.info('Migrated data directory from ~/.orchestrator to ~/.agent-workspace');
5261
}
62+
if (mergedLegacyDataDir.merged) {
63+
logger.info('Merged legacy ~/.orchestrator data into ~/.agent-workspace', {
64+
reason: mergedLegacyDataDir.reason,
65+
sourceDir: mergedLegacyDataDir.sourceDir,
66+
targetDir: mergedLegacyDataDir.targetDir,
67+
backupDir: mergedLegacyDataDir.backupDir,
68+
copiedCount: mergedLegacyDataDir.copied.length,
69+
overwrittenCount: mergedLegacyDataDir.overwritten.length
70+
});
71+
}
72+
if (legacyCompatibilityState.shouldUseLegacyDir) {
73+
logger.warn('Using legacy ~/.orchestrator data directory for backward compatibility', {
74+
reason: legacyCompatibilityState.reason,
75+
resolvedDataDir,
76+
oldWorkspaceCount: legacyCompatibilityState.oldWorkspaceCount,
77+
newWorkspaceCount: legacyCompatibilityState.newWorkspaceCount
78+
});
79+
}
5380
if (projectsRootBootstrap.usingLegacyProjectsRoot) {
5481
logger.info('Using legacy ~/GitHub as the projects root until ~/.agent-workspace/projects is populated', {
5582
projectsDir: projectsRootBootstrap.projectsDir

server/utils/pathUtils.js

Lines changed: 268 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,274 @@
11
const path = require('path');
22
const os = require('os');
33

4+
function getDefaultAgentWorkspaceDir() {
5+
return path.join(os.homedir(), '.agent-workspace');
6+
}
7+
8+
function getLegacyAgentWorkspaceDir() {
9+
return path.join(os.homedir(), '.orchestrator');
10+
}
11+
12+
function pathsResolveToSameLocation(a, b) {
13+
const fs = require('fs');
14+
try {
15+
return fs.realpathSync(a) === fs.realpathSync(b);
16+
} catch {
17+
return false;
18+
}
19+
}
20+
21+
function countVisibleEntries(dirPath) {
22+
const fs = require('fs');
23+
try {
24+
return fs.readdirSync(dirPath).filter((entry) => !String(entry || '').startsWith('.')).length;
25+
} catch {
26+
return 0;
27+
}
28+
}
29+
30+
function getWorkspaceEntryCount(baseDir) {
31+
return countVisibleEntries(path.join(baseDir, 'workspaces'));
32+
}
33+
34+
function isSameFileContent(sourcePath, targetPath) {
35+
const fs = require('fs');
36+
try {
37+
const sourceStat = fs.statSync(sourcePath);
38+
const targetStat = fs.statSync(targetPath);
39+
if (sourceStat.size !== targetStat.size) return false;
40+
const sourceContent = fs.readFileSync(sourcePath);
41+
const targetContent = fs.readFileSync(targetPath);
42+
return sourceContent.equals(targetContent);
43+
} catch {
44+
return false;
45+
}
46+
}
47+
48+
function copyTreeSync(sourcePath, targetPath) {
49+
const fs = require('fs');
50+
const stat = fs.lstatSync(sourcePath);
51+
if (stat.isDirectory()) {
52+
fs.mkdirSync(targetPath, { recursive: true });
53+
for (const entry of fs.readdirSync(sourcePath)) {
54+
copyTreeSync(path.join(sourcePath, entry), path.join(targetPath, entry));
55+
}
56+
return;
57+
}
58+
59+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
60+
fs.copyFileSync(sourcePath, targetPath);
61+
}
62+
63+
function mergeLegacyDataDir() {
64+
const fs = require('fs');
65+
66+
// Skip if env override is set (user chose a custom path)
67+
if (String(process.env.AGENT_WORKSPACE_DIR || '').trim()) {
68+
return { merged: false, reason: 'env-override' };
69+
}
70+
71+
const state = getLegacyCompatibilityState();
72+
if (!state.shouldUseLegacyDir) {
73+
return {
74+
merged: false,
75+
reason: state.reason,
76+
sourceDir: state.oldDir,
77+
targetDir: state.newDir
78+
};
79+
}
80+
81+
const sourceDir = state.oldDir;
82+
const targetDir = state.newDir;
83+
const backupRoot = path.join(
84+
targetDir,
85+
'migration-backups',
86+
`from-orchestrator-${new Date().toISOString().replace(/[:.]/g, '-')}`
87+
);
88+
const report = {
89+
merged: false,
90+
reason: state.reason,
91+
sourceDir,
92+
targetDir,
93+
backupDir: backupRoot,
94+
copied: [],
95+
overwritten: []
96+
};
97+
98+
if (!fs.existsSync(sourceDir)) {
99+
return { ...report, merged: false, reason: 'legacy-missing' };
100+
}
101+
102+
fs.mkdirSync(targetDir, { recursive: true });
103+
104+
const mergeEntry = (srcPath, dstPath, relPath = '') => {
105+
const srcStat = fs.lstatSync(srcPath);
106+
const dstExists = fs.existsSync(dstPath);
107+
108+
if (srcStat.isDirectory()) {
109+
if (!dstExists) {
110+
copyTreeSync(srcPath, dstPath);
111+
report.copied.push(relPath || path.basename(srcPath));
112+
return;
113+
}
114+
115+
const dstStat = fs.lstatSync(dstPath);
116+
if (!dstStat.isDirectory()) {
117+
const backupPath = path.join(backupRoot, relPath);
118+
copyTreeSync(dstPath, backupPath);
119+
report.overwritten.push(relPath);
120+
fs.rmSync(dstPath, { recursive: true, force: true });
121+
copyTreeSync(srcPath, dstPath);
122+
return;
123+
}
124+
125+
for (const entry of fs.readdirSync(srcPath)) {
126+
const nextRel = relPath ? path.join(relPath, entry) : entry;
127+
mergeEntry(path.join(srcPath, entry), path.join(dstPath, entry), nextRel);
128+
}
129+
return;
130+
}
131+
132+
if (!dstExists) {
133+
copyTreeSync(srcPath, dstPath);
134+
report.copied.push(relPath || path.basename(srcPath));
135+
return;
136+
}
137+
138+
const dstStat = fs.lstatSync(dstPath);
139+
if (dstStat.isDirectory() || !isSameFileContent(srcPath, dstPath)) {
140+
const backupPath = path.join(backupRoot, relPath);
141+
copyTreeSync(dstPath, backupPath);
142+
fs.mkdirSync(path.dirname(dstPath), { recursive: true });
143+
fs.copyFileSync(srcPath, dstPath);
144+
report.overwritten.push(relPath || path.basename(srcPath));
145+
}
146+
};
147+
148+
for (const entry of fs.readdirSync(sourceDir)) {
149+
if (entry === 'migration-backups') continue;
150+
mergeEntry(path.join(sourceDir, entry), path.join(targetDir, entry), entry);
151+
}
152+
153+
report.merged = report.copied.length > 0 || report.overwritten.length > 0;
154+
return report;
155+
}
156+
157+
function getLegacyCompatibilityState() {
158+
const fs = require('fs');
159+
const newDir = getDefaultAgentWorkspaceDir();
160+
const oldDir = getLegacyAgentWorkspaceDir();
161+
162+
if (!fs.existsSync(oldDir)) {
163+
return {
164+
shouldUseLegacyDir: false,
165+
newDir,
166+
oldDir,
167+
reason: 'legacy-missing'
168+
};
169+
}
170+
171+
if (pathsResolveToSameLocation(newDir, oldDir)) {
172+
return {
173+
shouldUseLegacyDir: false,
174+
newDir,
175+
oldDir,
176+
reason: 'already-linked'
177+
};
178+
}
179+
180+
if (!fs.existsSync(newDir)) {
181+
return {
182+
shouldUseLegacyDir: false,
183+
newDir,
184+
oldDir,
185+
reason: 'new-missing'
186+
};
187+
}
188+
189+
const oldWorkspaceCount = getWorkspaceEntryCount(oldDir);
190+
const newWorkspaceCount = getWorkspaceEntryCount(newDir);
191+
const oldRootCount = countVisibleEntries(oldDir);
192+
const newRootCount = countVisibleEntries(newDir);
193+
const oldHasUserData = oldWorkspaceCount > 0 || oldRootCount > 0;
194+
const newHasUserData = newWorkspaceCount > 0 || newRootCount > 0;
195+
196+
if (!oldHasUserData) {
197+
return {
198+
shouldUseLegacyDir: false,
199+
newDir,
200+
oldDir,
201+
reason: 'legacy-empty',
202+
oldWorkspaceCount,
203+
newWorkspaceCount,
204+
oldRootCount,
205+
newRootCount
206+
};
207+
}
208+
209+
if (!newHasUserData) {
210+
return {
211+
shouldUseLegacyDir: true,
212+
newDir,
213+
oldDir,
214+
reason: 'new-empty',
215+
oldWorkspaceCount,
216+
newWorkspaceCount,
217+
oldRootCount,
218+
newRootCount
219+
};
220+
}
221+
222+
if (oldWorkspaceCount > newWorkspaceCount) {
223+
return {
224+
shouldUseLegacyDir: true,
225+
newDir,
226+
oldDir,
227+
reason: 'legacy-has-more-workspaces',
228+
oldWorkspaceCount,
229+
newWorkspaceCount,
230+
oldRootCount,
231+
newRootCount
232+
};
233+
}
234+
235+
if (oldWorkspaceCount === newWorkspaceCount && oldRootCount > newRootCount) {
236+
return {
237+
shouldUseLegacyDir: true,
238+
newDir,
239+
oldDir,
240+
reason: 'legacy-has-more-state',
241+
oldWorkspaceCount,
242+
newWorkspaceCount,
243+
oldRootCount,
244+
newRootCount
245+
};
246+
}
247+
248+
return {
249+
shouldUseLegacyDir: false,
250+
newDir,
251+
oldDir,
252+
reason: 'prefer-new',
253+
oldWorkspaceCount,
254+
newWorkspaceCount,
255+
oldRootCount,
256+
newRootCount
257+
};
258+
}
259+
4260
/**
5261
* Root directory for Agent Workspace data (workspaces, configs, etc.).
6262
* Configurable via AGENT_WORKSPACE_DIR env var; defaults to ~/.agent-workspace
7263
*/
8264
function getAgentWorkspaceDir() {
9265
const envPath = String(process.env.AGENT_WORKSPACE_DIR || '').trim();
10266
if (envPath) return path.resolve(envPath);
11-
return path.join(os.homedir(), '.agent-workspace');
267+
const compatibility = getLegacyCompatibilityState();
268+
if (compatibility.shouldUseLegacyDir) {
269+
return compatibility.oldDir;
270+
}
271+
return compatibility.newDir;
12272
}
13273

14274
/**
@@ -33,8 +293,8 @@ function getLegacyProjectsRoot() {
33293
*/
34294
function migrateFromOrchestratorDir() {
35295
const fs = require('fs');
36-
const newDir = getAgentWorkspaceDir();
37-
const oldDir = path.join(os.homedir(), '.orchestrator');
296+
const newDir = getDefaultAgentWorkspaceDir();
297+
const oldDir = getLegacyAgentWorkspaceDir();
38298

39299
// Skip if env override is set (user chose a custom path)
40300
if (String(process.env.AGENT_WORKSPACE_DIR || '').trim()) return false;
@@ -61,12 +321,7 @@ function migrateFromOrchestratorDir() {
61321
}
62322

63323
function hasVisibleEntries(dirPath) {
64-
const fs = require('fs');
65-
try {
66-
return fs.readdirSync(dirPath).some((entry) => !String(entry || '').startsWith('.'));
67-
} catch {
68-
return false;
69-
}
324+
return countVisibleEntries(dirPath) > 0;
70325
}
71326

72327
/**
@@ -140,6 +395,10 @@ module.exports = {
140395
getPathBasename,
141396
getTrailingPathLabel,
142397
getAgentWorkspaceDir,
398+
getDefaultAgentWorkspaceDir,
399+
getLegacyAgentWorkspaceDir,
400+
getLegacyCompatibilityState,
401+
mergeLegacyDataDir,
143402
getProjectsRoot,
144403
getLegacyProjectsRoot,
145404
migrateFromOrchestratorDir,

0 commit comments

Comments
 (0)