farming-code 2.2.18 → 2.2.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/backend/server.js CHANGED
@@ -36,6 +36,7 @@ const {
36
36
  resumedAgentSource,
37
37
  } = require('./main-page-session');
38
38
  const { discoverAgentWorkspaces } = require('./workspace-discovery');
39
+ const { inspectGitWorktree } = require('./git-worktree-info');
39
40
  const { createWorkspaceDirectoryRouter } = require('./workspace-directory');
40
41
  const { createControlRouter } = require('./control-api');
41
42
  const { WorkspaceFileService, WorkspaceFileError } = require('./workspace-file-service');
@@ -743,8 +744,12 @@ app.get(routePath(BASE_PATH, '/api/claude/settings'), (req, res) => {
743
744
  app.get(routePath(BASE_PATH, '/api/usage'), async (req, res) => {
744
745
  try {
745
746
  const fresh = req.query.fresh === '1';
747
+ const live = req.query.live === '1';
746
748
  if (fresh) usageMonitor.invalidateDailyCache();
747
- const usage = await usageSummaryCache.get('summary', fresh ? { force: true } : {});
749
+ const usage = await usageSummaryCache.get(
750
+ 'summary',
751
+ fresh ? { force: true } : live ? { maxAgeMs: 15_000 } : {},
752
+ );
748
753
  res.json({ usage });
749
754
  } catch (error) {
750
755
  res.status(500).json({ error: error.message || 'Failed to read usage information' });
@@ -1433,6 +1438,40 @@ app.post(routePath(BASE_PATH, '/api/agents/:agentId/fork'), express.json(), asyn
1433
1438
  return;
1434
1439
  }
1435
1440
 
1441
+ try {
1442
+ const membership = configManager.mountProjectWorkspace(result.workspace);
1443
+ result.projectWorkspaces = membership.projectWorkspaces;
1444
+ result.pinnedProjectWorkspaces = membership.pinnedProjectWorkspaces;
1445
+ } catch (error) {
1446
+ let rollbackError = '';
1447
+ try {
1448
+ if (mode === 'new-worktree') {
1449
+ const rollback = await agentManager.deleteForkWorktreeProject(result.workspace, { force: true });
1450
+ if (!rollback?.deleted || rollback.cleanupError) {
1451
+ rollbackError = rollback?.error || rollback?.cleanupError || 'Failed to delete the fork worktree';
1452
+ }
1453
+ } else {
1454
+ const rollback = await agentManager.archiveAgent(result.agentId, {
1455
+ reason: 'project-mount-failed',
1456
+ recordHistory: false,
1457
+ requireEngineExit: true,
1458
+ scheduleProviderArchive: false,
1459
+ });
1460
+ if (rollback?.error) rollbackError = rollback.error;
1461
+ }
1462
+ } catch (cleanupError) {
1463
+ rollbackError = cleanupError?.message || String(cleanupError);
1464
+ }
1465
+ broadcastState();
1466
+ const mountError = error.message || 'Failed to create Project';
1467
+ res.status(500).json({
1468
+ error: rollbackError
1469
+ ? `${mountError}. Rollback failed: ${rollbackError}`
1470
+ : mountError,
1471
+ ...(rollbackError ? { rollbackError } : {}),
1472
+ });
1473
+ return;
1474
+ }
1436
1475
  broadcastState();
1437
1476
  res.status(201).json(result);
1438
1477
  });
@@ -1464,54 +1503,132 @@ app.post(routePath(BASE_PATH, '/api/projects/reveal'), express.json(), async (re
1464
1503
  }
1465
1504
  });
1466
1505
 
1506
+ app.post(routePath(BASE_PATH, '/api/projects/mount'), express.json(), async (req, res) => {
1507
+ try {
1508
+ const workspace = await canonicalProjectWorkspace(req.body?.workspace);
1509
+ const membership = configManager.mountProjectWorkspace(workspace);
1510
+ broadcastState();
1511
+ res.json(membership);
1512
+ } catch (error) {
1513
+ res.status(400).json({ error: error.message || 'Failed to create Project' });
1514
+ }
1515
+ });
1516
+
1517
+ app.post(routePath(BASE_PATH, '/api/projects/remove'), express.json(), (req, res) => {
1518
+ try {
1519
+ const membership = configManager.removeProjectWorkspace(req.body?.workspace);
1520
+ broadcastState();
1521
+ res.json(membership);
1522
+ } catch (error) {
1523
+ res.status(400).json({ error: error.message || 'Failed to remove Project' });
1524
+ }
1525
+ });
1526
+
1527
+ app.post(routePath(BASE_PATH, '/api/projects/pin'), express.json(), (req, res) => {
1528
+ try {
1529
+ const membership = configManager.setProjectWorkspacePinned(
1530
+ req.body?.workspace,
1531
+ req.body?.pinned === true
1532
+ );
1533
+ broadcastState();
1534
+ res.json(membership);
1535
+ } catch (error) {
1536
+ res.status(400).json({ error: error.message || 'Failed to update Project pin' });
1537
+ }
1538
+ });
1539
+
1467
1540
  app.post(routePath(BASE_PATH, '/api/projects/create-worktree'), express.json(), async (req, res) => {
1468
1541
  let created = null;
1469
- let previousProjects = null;
1470
1542
  try {
1471
1543
  const root = resolveProjectActionRoot(req.body?.rootId);
1472
1544
  created = await agentManager.createPermanentWorktree(root.canonicalPath);
1473
- previousProjects = configManager.getSettings().projectWorkspaces || [];
1474
- configManager.updateSettings({
1475
- projectWorkspaces: [
1476
- created.workspace,
1477
- ...previousProjects.filter(workspace => workspace !== created.workspace),
1478
- ],
1479
- });
1545
+ const membership = configManager.mountProjectWorkspace(created.workspace);
1546
+ broadcastState();
1480
1547
  res.status(201).json({
1481
1548
  workspace: created.workspace,
1482
1549
  branch: created.branch,
1483
- projectWorkspaces: configManager.getSettings().projectWorkspaces,
1550
+ projectWorkspaces: membership.projectWorkspaces,
1551
+ pinnedProjectWorkspaces: membership.pinnedProjectWorkspaces,
1484
1552
  });
1485
1553
  } catch (error) {
1486
- if (created) await agentManager.rollbackPermanentWorktree(created);
1487
- if (previousProjects) {
1488
- try {
1489
- configManager.updateSettings({ projectWorkspaces: previousProjects });
1490
- } catch {
1491
- // Keep the original error visible; the failed settings write is already the root cause.
1492
- }
1554
+ let rollbackError = '';
1555
+ if (created) {
1556
+ const rollback = await agentManager.rollbackPermanentWorktree(created);
1557
+ if (rollback?.rolledBack === false) rollbackError = rollback.error || 'Unknown rollback failure';
1493
1558
  }
1494
1559
  const status = error instanceof WorkspaceFileError ? error.statusCode : 400;
1495
- res.status(status).json({ error: error.message || 'Failed to create permanent worktree' });
1560
+ const createError = error.message || 'Failed to create permanent worktree';
1561
+ res.status(status).json({
1562
+ error: rollbackError
1563
+ ? `${createError}. Rollback failed: ${rollbackError}`
1564
+ : createError,
1565
+ ...(rollbackError ? { rollbackError } : {}),
1566
+ });
1496
1567
  }
1497
1568
  });
1498
1569
 
1499
1570
  app.post(routePath(BASE_PATH, '/api/projects/delete-worktree'), express.json(), async (req, res) => {
1500
1571
  const body = req.body || {};
1501
1572
  const workspace = typeof body.workspace === 'string' ? body.workspace : '';
1502
- const result = await agentManager.deleteForkWorktreeProject(workspace, { force: body.force === true });
1503
- if (result.error) {
1504
- if (result.requiresForce) {
1505
- res.status(409).json(result);
1573
+ const inspected = await agentManager.inspectForkWorktreeProject(workspace);
1574
+ if (inspected.error || (inspected.requiresForce && body.force !== true)) {
1575
+ if (inspected.requiresForce) {
1576
+ res.status(409).json({
1577
+ ...inspected,
1578
+ error: 'Worktree has uncommitted or untracked files',
1579
+ });
1506
1580
  return;
1507
1581
  }
1508
- const status = result.error === 'Workspace not found' || result.error === 'Workspace is required' ? 404 : 400;
1509
- res.status(status).json(result);
1582
+ const status = inspected.error === 'Workspace not found' || inspected.error === 'Workspace is required' ? 404 : 400;
1583
+ res.status(status).json(inspected);
1510
1584
  return;
1511
1585
  }
1512
1586
 
1587
+ const wasPinned = (configManager.getSettings().pinnedProjectWorkspaces || []).includes(inspected.workspace);
1588
+ let membership;
1589
+ try {
1590
+ membership = configManager.removeProjectWorkspace(inspected.workspace);
1591
+ } catch (error) {
1592
+ res.status(500).json({
1593
+ ...inspected,
1594
+ error: error.message || 'Project removal failed; the worktree was not deleted',
1595
+ });
1596
+ return;
1597
+ }
1598
+ const result = await agentManager.deleteForkWorktreeProject(inspected.workspace, { force: body.force === true });
1599
+ if (result.error) {
1600
+ let rollbackError = '';
1601
+ try {
1602
+ configManager.mountProjectWorkspace(inspected.workspace);
1603
+ if (wasPinned) configManager.setProjectWorkspacePinned(inspected.workspace, true);
1604
+ } catch (error) {
1605
+ rollbackError = error.message || String(error);
1606
+ }
1607
+ broadcastState();
1608
+ const status = result.requiresForce ? 409 : 400;
1609
+ res.status(status).json({
1610
+ ...result,
1611
+ ...(rollbackError ? {
1612
+ error: `${result.error}. Project restore failed: ${rollbackError}`,
1613
+ rollbackError,
1614
+ } : {}),
1615
+ });
1616
+ return;
1617
+ }
1618
+ if (result.cleanupError) {
1619
+ broadcastState();
1620
+ res.status(500).json({
1621
+ ...result,
1622
+ error: `Worktree was deleted, but Agent cleanup failed: ${result.cleanupError}`,
1623
+ });
1624
+ return;
1625
+ }
1513
1626
  broadcastState();
1514
- res.json(result);
1627
+ res.json({
1628
+ ...result,
1629
+ projectWorkspaces: membership.projectWorkspaces,
1630
+ pinnedProjectWorkspaces: membership.pinnedProjectWorkspaces,
1631
+ });
1515
1632
  });
1516
1633
 
1517
1634
  app.post(routePath(BASE_PATH, '/api/codex/sessions/:sessionId/resume'), express.json(), async (req, res) => {
@@ -1523,6 +1640,7 @@ app.post(routePath(BASE_PATH, '/api/agent-sessions/:provider/:sessionId/resume')
1523
1640
  });
1524
1641
 
1525
1642
  const pendingResumeStarts = new Map();
1643
+ const pendingProjectWorkspaceResolutions = new Map();
1526
1644
 
1527
1645
  function resumedAgentStartKey(provider, sessionId, options = {}) {
1528
1646
  return [
@@ -1584,6 +1702,14 @@ function forgetMainPageAgentSession(provider, sessionId, providerHomeId = '') {
1584
1702
  });
1585
1703
  }
1586
1704
 
1705
+ function savedFarmingAgentSession(provider, sessionId, providerHomeId = '') {
1706
+ if (typeof configManager.getAgentSessionRecordForProviderSessionKey !== 'function') return null;
1707
+ const sessionKey = mainPageAgentSessionKey(provider, sessionId, providerHomeId);
1708
+ return sessionKey
1709
+ ? configManager.getAgentSessionRecordForProviderSessionKey(sessionKey)
1710
+ : null;
1711
+ }
1712
+
1587
1713
  async function resumeAgentSessionById(provider, rawSessionId, options = {}) {
1588
1714
  const normalizedProvider = normalizeProvider(provider);
1589
1715
  const sessionId = String(rawSessionId || '').trim();
@@ -1604,7 +1730,12 @@ async function resumeAgentSessionById(provider, rawSessionId, options = {}) {
1604
1730
  const existingAgent = findResumedAgent(normalizedProvider, sessionId, providerHomeId);
1605
1731
  if (existingAgent) {
1606
1732
  if (shouldRememberMainPageSession) rememberMainPageAgentSession(normalizedProvider, sessionId, providerHomeId);
1607
- return { agentId: existingAgent.id, reused: true };
1733
+ const projectWorkspace = requestedAsMain
1734
+ ? ''
1735
+ : await canonicalProjectWorkspace(
1736
+ existingAgent.gitWorktree?.workspace || existingAgent.projectWorkspace || existingAgent.cwd
1737
+ );
1738
+ return { agentId: existingAgent.id, projectWorkspace, reused: true };
1608
1739
  }
1609
1740
  }
1610
1741
  const pendingStart = pendingResumeStarts.get(pendingResumeId);
@@ -1616,6 +1747,7 @@ async function resumeAgentSessionById(provider, rawSessionId, options = {}) {
1616
1747
  if (shouldRememberMainPageSession) rememberMainPageAgentSession(normalizedProvider, sessionId, providerHomeId);
1617
1748
  return {
1618
1749
  agentId: result.agentId,
1750
+ projectWorkspace: result.projectWorkspace || '',
1619
1751
  reused: true,
1620
1752
  pending: true,
1621
1753
  ...(result.claimed ? { claimed: true } : {}),
@@ -1663,7 +1795,10 @@ async function resumeAgentSessionById(provider, rawSessionId, options = {}) {
1663
1795
  });
1664
1796
  if (claimingAgent) {
1665
1797
  if (shouldRememberMainPageSession) rememberMainPageAgentSession(normalizedProvider, sessionId, providerHomeId);
1666
- return { agentId: claimingAgent.id, reused: true, claimed: true };
1798
+ const projectWorkspace = await canonicalProjectWorkspace(
1799
+ claimingAgent.gitWorktree?.workspace || claimingAgent.projectWorkspace || claimingAgent.cwd
1800
+ );
1801
+ return { agentId: claimingAgent.id, projectWorkspace, reused: true, claimed: true };
1667
1802
  }
1668
1803
  }
1669
1804
 
@@ -1672,7 +1807,20 @@ async function resumeAgentSessionById(provider, rawSessionId, options = {}) {
1672
1807
  return { error: 'session is not a Main Agent session', status: 400 };
1673
1808
  }
1674
1809
 
1810
+ const savedSession = shouldFork
1811
+ ? null
1812
+ : savedFarmingAgentSession(normalizedProvider, sessionId, session
1813
+ ? (session.providerHomeId || providerHomeId)
1814
+ : providerHomeId);
1815
+ const hasRequestedCustomTitle = Object.prototype.hasOwnProperty.call(options, 'customTitle');
1816
+ const savedAttentionSeq = Number(savedSession?.attentionSeq) || 0;
1817
+ const savedReadAttentionSeq = Number(savedSession?.readAttentionSeq) || 0;
1675
1818
  const workingDirectory = session && (session.cwd || session.workspace) ? (session.cwd || session.workspace) : null;
1819
+ const projectWorkspace = resumeAsMain
1820
+ ? ''
1821
+ : await canonicalProjectWorkspace(
1822
+ savedSession?.projectWorkspace || (session ? (session.workspace || session.cwd || '') : workingDirectory)
1823
+ );
1676
1824
  const command = buildAgentSessionResumeCommand(normalizedProvider, sessionId, {
1677
1825
  fork: shouldFork,
1678
1826
  cwd: workingDirectory,
@@ -1699,19 +1847,38 @@ async function resumeAgentSessionById(provider, rawSessionId, options = {}) {
1699
1847
  return;
1700
1848
  }
1701
1849
 
1702
- resolve({ agentId });
1850
+ resolve({ agentId, projectWorkspace });
1703
1851
  }, {
1704
1852
  wantsMain: resumeAsMain,
1705
- task: session ? session.title : '',
1706
- customTitle: typeof options.customTitle === 'string' ? options.customTitle : '',
1853
+ task: savedSession?.task || (session ? session.title : ''),
1854
+ workflowTemplate: savedSession?.workflowTemplate || '',
1855
+ customTitle: hasRequestedCustomTitle
1856
+ ? (typeof options.customTitle === 'string' ? options.customTitle : '')
1857
+ : (savedSession?.customTitle || ''),
1858
+ customTitleExplicit: hasRequestedCustomTitle,
1707
1859
  requiredCliVersion: normalizedProvider === 'codex' && session ? session.cliVersion : '',
1708
- projectWorkspace: session ? (session.workspace || session.cwd || '') : '',
1860
+ projectWorkspace,
1709
1861
  source: shouldFork ? resumeSource.replace('-history:', '-history-fork:') : resumeSource,
1710
1862
  agentRuntimeMode: ['chat', 'acp'].includes(options.agentRuntimeMode) ? 'chat' : 'terminal',
1711
1863
  acpHistoryMode: options.acpHistoryMode === 'resume' ? 'resume' : 'load',
1712
1864
  providerHomeId: resolvedProviderHomeId,
1713
1865
  providerHomePath: session ? (session.providerHomePath || '') : '',
1714
- autoReadInitialAttention: options.autoReadInitialAttention === true,
1866
+ providerSessionTitle: session?.title || savedSession?.providerSessionTitle || '',
1867
+ persistentSessionId: savedSession?.id || '',
1868
+ pinned: savedSession?.pinned === true,
1869
+ projectOrder: savedSession?.projectOrder,
1870
+ pinnedOrder: savedSession?.pinnedOrder,
1871
+ attentionSeq: savedAttentionSeq,
1872
+ readAttentionSeq: savedReadAttentionSeq,
1873
+ attentionUpdatedAt: savedSession?.attentionUpdatedAt,
1874
+ readAttentionAt: savedSession?.readAttentionAt,
1875
+ attentionReason: savedSession?.attentionReason,
1876
+ attentionOutputEpoch: savedSession?.attentionOutputEpoch,
1877
+ attentionOutputSeq: savedSession?.attentionOutputSeq,
1878
+ readOutputEpoch: savedSession?.readOutputEpoch,
1879
+ readOutputSeq: savedSession?.readOutputSeq,
1880
+ autoReadInitialAttention: options.autoReadInitialAttention === true
1881
+ && savedAttentionSeq <= savedReadAttentionSeq,
1715
1882
  preserveProviderSessionProfile: normalizedProvider === 'codex',
1716
1883
  });
1717
1884
  Promise.resolve(startResult).catch((error) => {
@@ -1734,23 +1901,73 @@ async function resumeAgentSessionById(provider, rawSessionId, options = {}) {
1734
1901
  if (result.reused) {
1735
1902
  return {
1736
1903
  agentId: result.agentId,
1904
+ projectWorkspace: result.projectWorkspace || '',
1737
1905
  reused: true,
1738
1906
  ...(result.claimed ? { claimed: true } : {}),
1739
1907
  };
1740
1908
  }
1741
- return { agentId: result.agentId };
1909
+ return {
1910
+ agentId: result.agentId,
1911
+ projectWorkspace: result.projectWorkspace || '',
1912
+ };
1913
+ }
1914
+
1915
+ async function canonicalProjectWorkspace(workspace) {
1916
+ const candidate = configManager.expandWorkspacePath(String(workspace || '').trim());
1917
+ if (!candidate) return '';
1918
+ const pending = pendingProjectWorkspaceResolutions.get(candidate);
1919
+ if (pending) return pending;
1920
+
1921
+ const resolution = (async () => {
1922
+ try {
1923
+ const worktree = await inspectGitWorktree(candidate);
1924
+ if (worktree?.workspace) return worktree.workspace;
1925
+ } catch (error) {
1926
+ console.warn('Failed to resolve project worktree:', candidate, error?.message || error);
1927
+ }
1928
+ try {
1929
+ return fs.realpathSync(path.resolve(candidate));
1930
+ } catch {
1931
+ return candidate;
1932
+ }
1933
+ })();
1934
+ pendingProjectWorkspaceResolutions.set(candidate, resolution);
1935
+ try {
1936
+ return await resolution;
1937
+ } finally {
1938
+ if (pendingProjectWorkspaceResolutions.get(candidate) === resolution) {
1939
+ pendingProjectWorkspaceResolutions.delete(candidate);
1940
+ }
1941
+ }
1742
1942
  }
1743
1943
 
1744
1944
  async function startResumedAgentSession(req, res, provider, rawSessionId) {
1745
1945
  const shouldFork = req.body && req.body.fork === true;
1746
1946
  const requestedAsMain = req.body && req.body.asMain === true && !shouldFork;
1747
1947
  const allowUnarchiveArchived = req.body && req.body.unarchiveArchived === true && !shouldFork && !requestedAsMain;
1948
+ const providerHomeId = req.body && typeof req.body.providerHomeId === 'string'
1949
+ ? req.body.providerHomeId
1950
+ : '';
1951
+ const sessionKey = mainPageAgentSessionKey(provider, rawSessionId, providerHomeId);
1952
+ const mainPageSessionWasRemembered = !shouldFork
1953
+ && !requestedAsMain
1954
+ && configManager.getMainPageSessionKeys().includes(sessionKey);
1955
+ if (
1956
+ req.body
1957
+ && Object.prototype.hasOwnProperty.call(req.body, 'customTitle')
1958
+ && typeof req.body.customTitle !== 'string'
1959
+ ) {
1960
+ res.status(400).json({ error: 'customTitle must be a string' });
1961
+ return;
1962
+ }
1748
1963
  const result = await resumeAgentSessionById(provider, rawSessionId, {
1749
1964
  fork: shouldFork,
1750
1965
  asMain: requestedAsMain,
1751
1966
  allowUnarchiveArchived,
1752
- providerHomeId: req.body && typeof req.body.providerHomeId === 'string' ? req.body.providerHomeId : '',
1753
- customTitle: req.body && typeof req.body.customTitle === 'string' ? req.body.customTitle : '',
1967
+ providerHomeId,
1968
+ ...(req.body && Object.prototype.hasOwnProperty.call(req.body, 'customTitle')
1969
+ ? { customTitle: req.body.customTitle }
1970
+ : {}),
1754
1971
  agentRuntimeMode: req.body && ['chat', 'acp'].includes(req.body.agentRuntimeMode) ? 'chat' : 'terminal',
1755
1972
  acpHistoryMode: req.body && req.body.acpHistoryMode === 'resume' ? 'resume' : 'load',
1756
1973
  });
@@ -1760,10 +1977,49 @@ async function startResumedAgentSession(req, res, provider, rawSessionId) {
1760
1977
  return;
1761
1978
  }
1762
1979
 
1980
+ let projectMembership = {
1981
+ projectWorkspaces: configManager.getSettings().projectWorkspaces || [],
1982
+ pinnedProjectWorkspaces: configManager.getSettings().pinnedProjectWorkspaces || [],
1983
+ };
1984
+ try {
1985
+ if (!requestedAsMain && result.projectWorkspace) {
1986
+ projectMembership = configManager.mountProjectWorkspace(result.projectWorkspace);
1987
+ }
1988
+ } catch (error) {
1989
+ let rollbackError = '';
1990
+ if (!result.reused) {
1991
+ try {
1992
+ const rollback = await agentManager.archiveAgent(result.agentId, {
1993
+ reason: 'project-mount-failed',
1994
+ recordHistory: false,
1995
+ requireEngineExit: true,
1996
+ scheduleProviderArchive: false,
1997
+ });
1998
+ if (rollback?.error) rollbackError = rollback.error;
1999
+ } catch (cleanupError) {
2000
+ rollbackError = cleanupError?.message || String(cleanupError);
2001
+ }
2002
+ }
2003
+ if (!mainPageSessionWasRemembered && !rollbackError) {
2004
+ forgetMainPageAgentSession(provider, rawSessionId, providerHomeId || 'default');
2005
+ }
2006
+ broadcastState();
2007
+ const mountError = error.message || 'Failed to create Project';
2008
+ res.status(500).json({
2009
+ error: rollbackError
2010
+ ? `${mountError}. Rollback failed: ${rollbackError}`
2011
+ : mountError,
2012
+ ...(rollbackError ? { rollbackError } : {}),
2013
+ });
2014
+ return;
2015
+ }
1763
2016
  broadcastState();
1764
2017
  if (result.reused) {
1765
2018
  res.status(200).json({
1766
2019
  agentId: result.agentId,
2020
+ ...(result.projectWorkspace ? { projectWorkspace: result.projectWorkspace } : {}),
2021
+ projectWorkspaces: projectMembership.projectWorkspaces,
2022
+ pinnedProjectWorkspaces: projectMembership.pinnedProjectWorkspaces,
1767
2023
  reused: true,
1768
2024
  ...(result.claimed ? { claimed: true } : {}),
1769
2025
  ...(result.pending ? { pending: true } : {}),
@@ -1771,7 +2027,12 @@ async function startResumedAgentSession(req, res, provider, rawSessionId) {
1771
2027
  return;
1772
2028
  }
1773
2029
 
1774
- res.status(201).json({ agentId: result.agentId });
2030
+ res.status(201).json({
2031
+ agentId: result.agentId,
2032
+ ...(result.projectWorkspace ? { projectWorkspace: result.projectWorkspace } : {}),
2033
+ projectWorkspaces: projectMembership.projectWorkspaces,
2034
+ pinnedProjectWorkspaces: projectMembership.pinnedProjectWorkspaces,
2035
+ });
1775
2036
  }
1776
2037
 
1777
2038
  async function autoResumeMainPageAgentSessions() {
@@ -1847,7 +2108,10 @@ async function autoResumeMainPageAgentSessions() {
1847
2108
  }
1848
2109
 
1849
2110
  app.post(routePath(BASE_PATH, '/api/settings'), express.json(), (req, res) => {
1850
- configManager.updateSettings(req.body || {});
2111
+ const settingsPatch = { ...(req.body || {}) };
2112
+ delete settingsPatch.projectWorkspaces;
2113
+ delete settingsPatch.pinnedProjectWorkspaces;
2114
+ configManager.updateSettings(settingsPatch);
1851
2115
  agentSessionsCache.invalidate();
1852
2116
  res.json({
1853
2117
  success: true,
@@ -2089,27 +2353,70 @@ function handleMessage(ws, data) {
2089
2353
  break;
2090
2354
  case 'start-agent': {
2091
2355
  const workspace = data.workspace || null;
2092
- agentManager.startAgent(data.command, workspace, (agentId, error) => {
2093
- if (error) {
2094
- ws.send(JSON.stringify({ type: 'error', message: error }));
2095
- } else if (agentId) {
2096
- ws.agentId = agentId;
2097
- broadcastState();
2098
- ws.send(JSON.stringify({ type: 'agent-started', agentId }));
2099
- }
2100
- }, {
2101
- wantsMain: data.asMain === true,
2102
- projectWorkspace: typeof data.projectWorkspace === 'string' ? data.projectWorkspace : '',
2103
- task: typeof data.task === 'string' ? data.task : '',
2104
- workflowTemplate: typeof data.workflowTemplate === 'string' ? data.workflowTemplate : '',
2105
- customTitle: typeof data.customTitle === 'string' ? data.customTitle : '',
2106
- codexApprovalMode: typeof data.codexApprovalMode === 'string' ? data.codexApprovalMode : undefined,
2107
- agentRuntimeMode: ['json', 'acp', 'chat'].includes(data.agentRuntimeMode) ? data.agentRuntimeMode : 'terminal',
2108
- acpHistoryMode: data.acpHistoryMode === 'resume' ? 'resume' : 'load',
2109
- providerHomeId: typeof data.providerHomeId === 'string' ? data.providerHomeId : '',
2110
- ...(Array.isArray(data.additionalDirectories) ? { additionalDirectories: data.additionalDirectories } : {}),
2111
- ...(Array.isArray(data.mcpServers) ? { mcpServers: data.mcpServers } : {}),
2112
- ...(data.dangerouslySkipPermissions === true ? { dangerouslySkipPermissions: true } : {}),
2356
+ void (async () => {
2357
+ const projectWorkspace = data.asMain === true
2358
+ ? ''
2359
+ : await canonicalProjectWorkspace(
2360
+ typeof data.projectWorkspace === 'string' && data.projectWorkspace.trim()
2361
+ ? data.projectWorkspace
2362
+ : workspace
2363
+ );
2364
+ agentManager.startAgent(data.command, workspace, (agentId, error) => {
2365
+ if (error) {
2366
+ ws.send(JSON.stringify({ type: 'error', message: error }));
2367
+ } else if (agentId) {
2368
+ void (async () => {
2369
+ try {
2370
+ if (projectWorkspace) configManager.mountProjectWorkspace(projectWorkspace);
2371
+ } catch (mountError) {
2372
+ let rollbackError = '';
2373
+ try {
2374
+ const rollback = await agentManager.archiveAgent(agentId, {
2375
+ reason: 'project-mount-failed',
2376
+ recordHistory: false,
2377
+ requireEngineExit: true,
2378
+ scheduleProviderArchive: false,
2379
+ });
2380
+ if (rollback?.error) rollbackError = rollback.error;
2381
+ } catch (cleanupError) {
2382
+ rollbackError = cleanupError?.message || String(cleanupError);
2383
+ }
2384
+ broadcastState();
2385
+ const errorMessage = mountError.message || 'Failed to create Project';
2386
+ ws.send(JSON.stringify({
2387
+ type: 'error',
2388
+ message: rollbackError
2389
+ ? `${errorMessage}. Rollback failed: ${rollbackError}`
2390
+ : errorMessage,
2391
+ }));
2392
+ return;
2393
+ }
2394
+ ws.agentId = agentId;
2395
+ broadcastState();
2396
+ ws.send(JSON.stringify({ type: 'agent-started', agentId }));
2397
+ })().catch(callbackError => {
2398
+ console.warn('Failed to finish started Agent transition:', agentId, callbackError?.message || callbackError);
2399
+ });
2400
+ }
2401
+ }, {
2402
+ wantsMain: data.asMain === true,
2403
+ projectWorkspace,
2404
+ task: typeof data.task === 'string' ? data.task : '',
2405
+ workflowTemplate: typeof data.workflowTemplate === 'string' ? data.workflowTemplate : '',
2406
+ customTitle: typeof data.customTitle === 'string' ? data.customTitle : '',
2407
+ codexApprovalMode: typeof data.codexApprovalMode === 'string' ? data.codexApprovalMode : undefined,
2408
+ agentRuntimeMode: ['json', 'acp', 'chat'].includes(data.agentRuntimeMode) ? data.agentRuntimeMode : 'terminal',
2409
+ acpHistoryMode: data.acpHistoryMode === 'resume' ? 'resume' : 'load',
2410
+ providerHomeId: typeof data.providerHomeId === 'string' ? data.providerHomeId : '',
2411
+ ...(Array.isArray(data.additionalDirectories) ? { additionalDirectories: data.additionalDirectories } : {}),
2412
+ ...(Array.isArray(data.mcpServers) ? { mcpServers: data.mcpServers } : {}),
2413
+ ...(data.dangerouslySkipPermissions === true ? { dangerouslySkipPermissions: true } : {}),
2414
+ });
2415
+ })().catch(error => {
2416
+ ws.send(JSON.stringify({
2417
+ type: 'error',
2418
+ message: error.message || 'Failed to resolve Project workspace',
2419
+ }));
2113
2420
  });
2114
2421
  break;
2115
2422
  }
@@ -2262,6 +2569,7 @@ async function watchWorkspaceFiles(ws, data) {
2262
2569
 
2263
2570
  function buildStatePayload() {
2264
2571
  const state = agentManager.getState();
2572
+ const settings = configManager.getSettings();
2265
2573
  return {
2266
2574
  ...state,
2267
2575
  agents: state.agents.map(agent => ({
@@ -2271,7 +2579,9 @@ function buildStatePayload() {
2271
2579
  workspaceRoots: workspaceRootRegistry.list(),
2272
2580
  mainPageSessionKeys: typeof configManager.getMainPageSessionKeys === 'function'
2273
2581
  ? configManager.getMainPageSessionKeys()
2274
- : (Array.isArray(configManager.getSettings().mainPageSessionKeys) ? configManager.getSettings().mainPageSessionKeys : []),
2582
+ : (Array.isArray(settings.mainPageSessionKeys) ? settings.mainPageSessionKeys : []),
2583
+ projectWorkspaces: settings.projectWorkspaces || [],
2584
+ pinnedProjectWorkspaces: settings.pinnedProjectWorkspaces || [],
2275
2585
  };
2276
2586
  }
2277
2587
 
@@ -23,6 +23,7 @@ class TerminalScreenWorkerPool {
23
23
  this.waiters = [];
24
24
  this.readyWaiters = [];
25
25
  this.pendingStarts = 0;
26
+ this.startTasks = new Set();
26
27
  this.consecutiveStartFailures = 0;
27
28
  this.retryDelayMs = Number.isFinite(options.retryDelayMs)
28
29
  ? Math.max(0, Number(options.retryDelayMs))
@@ -91,13 +92,12 @@ class TerminalScreenWorkerPool {
91
92
  startWorker() {
92
93
  this.pendingStarts += 1;
93
94
  let failed = false;
94
- this.createReadyWorker()
95
+ const startTask = this.createReadyWorker()
95
96
  .then((worker) => {
96
97
  this.pendingStarts -= 1;
97
98
  this.consecutiveStartFailures = 0;
98
99
  if (this.disposed) {
99
- worker.dispose().catch(() => {});
100
- return;
100
+ return worker.dispose().catch(() => {});
101
101
  }
102
102
  this.deliverWorker(worker);
103
103
  })
@@ -122,6 +122,11 @@ class TerminalScreenWorkerPool {
122
122
  this.ensureCapacity();
123
123
  }
124
124
  });
125
+ this.startTasks.add(startTask);
126
+ startTask.then(
127
+ () => this.startTasks.delete(startTask),
128
+ () => this.startTasks.delete(startTask),
129
+ );
125
130
  }
126
131
 
127
132
  scheduleCapacityRetry() {
@@ -197,8 +202,8 @@ class TerminalScreenWorkerPool {
197
202
  }
198
203
 
199
204
  async dispose() {
200
- if (this.disposed) {
201
- return;
205
+ if (this.disposePromise) {
206
+ return this.disposePromise;
202
207
  }
203
208
 
204
209
  this.disposed = true;
@@ -210,7 +215,11 @@ class TerminalScreenWorkerPool {
210
215
  this.waiters.splice(0).forEach(({ reject }) => reject(error));
211
216
  this.readyWaiters.splice(0).forEach(({ reject }) => reject(error));
212
217
  const workers = this.idle.splice(0);
213
- await Promise.allSettled(workers.map(worker => worker.dispose()));
218
+ this.disposePromise = Promise.allSettled([
219
+ ...workers.map(worker => worker.dispose()),
220
+ ...this.startTasks,
221
+ ]).then(() => undefined);
222
+ return this.disposePromise;
214
223
  }
215
224
  }
216
225