groove-dev 0.27.161 → 0.27.164
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/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/gateways/manager.js +1 -0
- package/node_modules/@groove-dev/daemon/src/index.js +3 -0
- package/node_modules/@groove-dev/daemon/src/model-lab.js +15 -0
- package/node_modules/@groove-dev/daemon/src/process.js +227 -105
- package/node_modules/@groove-dev/daemon/src/routes/teams.js +2 -0
- package/node_modules/@groove-dev/daemon/src/scheduler.js +1 -0
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +4 -8
- package/node_modules/@groove-dev/gui/dist/assets/index-BJVNpGIp.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-CkCFf4Fl.js +1025 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/App.jsx +2 -0
- package/node_modules/@groove-dev/gui/src/components/agents/diff-viewer.jsx +20 -12
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +5 -2
- package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +2 -19
- package/node_modules/@groove-dev/gui/src/components/fleet/fleet-agent-row.jsx +176 -0
- package/node_modules/@groove-dev/gui/src/components/fleet/fleet-content.jsx +135 -0
- package/node_modules/@groove-dev/gui/src/components/fleet/fleet-pane.jsx +105 -0
- package/node_modules/@groove-dev/gui/src/components/fleet/fleet-sidebar.jsx +216 -0
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +2 -1
- package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +4 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/ui-slice.js +71 -1
- package/node_modules/@groove-dev/gui/src/views/fleet.jsx +15 -0
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/gateways/manager.js +1 -0
- package/packages/daemon/src/index.js +3 -0
- package/packages/daemon/src/model-lab.js +15 -0
- package/packages/daemon/src/process.js +227 -105
- package/packages/daemon/src/routes/teams.js +2 -0
- package/packages/daemon/src/scheduler.js +1 -0
- package/packages/daemon/src/tunnel-manager.js +4 -8
- package/packages/gui/dist/assets/index-BJVNpGIp.css +1 -0
- package/packages/gui/dist/assets/index-CkCFf4Fl.js +1025 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/App.jsx +2 -0
- package/packages/gui/src/components/agents/diff-viewer.jsx +20 -12
- package/packages/gui/src/components/agents/spawn-wizard.jsx +5 -2
- package/packages/gui/src/components/agents/workspace-mode.jsx +2 -19
- package/packages/gui/src/components/fleet/fleet-agent-row.jsx +176 -0
- package/packages/gui/src/components/fleet/fleet-content.jsx +135 -0
- package/packages/gui/src/components/fleet/fleet-pane.jsx +105 -0
- package/packages/gui/src/components/fleet/fleet-sidebar.jsx +216 -0
- package/packages/gui/src/components/layout/activity-bar.jsx +2 -1
- package/packages/gui/src/stores/slices/agents-slice.js +4 -0
- package/packages/gui/src/stores/slices/ui-slice.js +71 -1
- package/packages/gui/src/views/fleet.jsx +15 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-DpRdb7o1.js +0 -1020
- package/node_modules/@groove-dev/gui/dist/assets/index-Dzofq3wS.css +0 -1
- package/packages/gui/dist/assets/index-DpRdb7o1.js +0 -1020
- package/packages/gui/dist/assets/index-Dzofq3wS.css +0 -1
|
@@ -349,6 +349,7 @@ export class ProcessManager {
|
|
|
349
349
|
this._reviewTriggered = new Set(); // teamIds that have had review triggered (one round only)
|
|
350
350
|
this._pendingReviews = new Map(); // teamId -> { reviewAgentId }
|
|
351
351
|
this._reviewPending = new Set(); // teamIds with review in progress (blocks preview launch)
|
|
352
|
+
this._phase2Debounce = new Map(); // groupIndex -> timer (300ms settle delay)
|
|
352
353
|
|
|
353
354
|
this._stallWatchdog = setInterval(() => this._checkStalls(), STALL_CHECK_INTERVAL_MS);
|
|
354
355
|
if (this._stallWatchdog.unref) this._stallWatchdog.unref();
|
|
@@ -1850,6 +1851,7 @@ For normal file edits within your scope, proceed without review.
|
|
|
1850
1851
|
teamId: defaultTeamId,
|
|
1851
1852
|
})),
|
|
1852
1853
|
});
|
|
1854
|
+
this._persistPendingPhase2();
|
|
1853
1855
|
}
|
|
1854
1856
|
|
|
1855
1857
|
// Clean up the file
|
|
@@ -1867,133 +1869,253 @@ For normal file edits within your scope, proceed without review.
|
|
|
1867
1869
|
/**
|
|
1868
1870
|
* Check if a completed/crashed agent was the last phase 1 agent in a team.
|
|
1869
1871
|
* If so, auto-spawn the phase 2 (QC/finisher) agents.
|
|
1872
|
+
*
|
|
1873
|
+
* Uses a 300ms debounce so rapid parallel completions settle before the
|
|
1874
|
+
* allDone check, and defers splicing the pending group until spawns succeed.
|
|
1870
1875
|
*/
|
|
1871
1876
|
_checkPhase2(completedAgentId) {
|
|
1872
1877
|
const pending = this.daemon._pendingPhase2;
|
|
1873
|
-
if (!pending || pending.length === 0)
|
|
1874
|
-
|
|
1875
|
-
|
|
1878
|
+
if (!pending || pending.length === 0) {
|
|
1879
|
+
console.log(`[Groove:Phase2] _checkPhase2(${completedAgentId}): no pending groups, skipping`);
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1876
1882
|
|
|
1877
1883
|
for (let i = pending.length - 1; i >= 0; i--) {
|
|
1878
1884
|
const group = pending[i];
|
|
1879
1885
|
if (!group.waitFor.includes(completedAgentId)) continue;
|
|
1880
1886
|
|
|
1881
|
-
//
|
|
1882
|
-
|
|
1887
|
+
// Debounce: cancel any existing timer for this group and restart.
|
|
1888
|
+
// This lets parallel agent completions (50-100ms apart) settle
|
|
1889
|
+
// before we evaluate the allDone condition.
|
|
1890
|
+
const debounceKey = group.waitFor.join(',');
|
|
1891
|
+
const existingTimer = this._phase2Debounce.get(debounceKey);
|
|
1892
|
+
if (existingTimer) clearTimeout(existingTimer);
|
|
1893
|
+
|
|
1894
|
+
const groupRef = group;
|
|
1895
|
+
const timer = setTimeout(() => {
|
|
1896
|
+
this._phase2Debounce.delete(debounceKey);
|
|
1897
|
+
this._executePhase2Check(completedAgentId, groupRef);
|
|
1898
|
+
}, 300);
|
|
1899
|
+
this._phase2Debounce.set(debounceKey, timer);
|
|
1900
|
+
console.log(`[Groove:Phase2] _checkPhase2(${completedAgentId}): debounce scheduled for group [${debounceKey}]`);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
_executePhase2Check(completedAgentId, group) {
|
|
1905
|
+
const pending = this.daemon._pendingPhase2;
|
|
1906
|
+
if (!pending || !pending.includes(group)) {
|
|
1907
|
+
console.log(`[Groove:Phase2] _executePhase2Check: group no longer in pending queue, skipping`);
|
|
1908
|
+
return;
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
if (group._spawning) {
|
|
1912
|
+
console.log(`[Groove:Phase2] _executePhase2Check: group already spawning, skipping`);
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
const registry = this.daemon.registry;
|
|
1917
|
+
const allDone = group.waitFor.every((id) => {
|
|
1918
|
+
const a = registry.get(id);
|
|
1919
|
+
return !a || a.status === 'completed' || a.status === 'crashed' || a.status === 'stopped' || a.status === 'killed';
|
|
1920
|
+
});
|
|
1921
|
+
|
|
1922
|
+
if (!allDone) {
|
|
1923
|
+
const still = group.waitFor.filter((id) => {
|
|
1883
1924
|
const a = registry.get(id);
|
|
1884
|
-
return
|
|
1925
|
+
return a && a.status !== 'completed' && a.status !== 'crashed' && a.status !== 'stopped' && a.status !== 'killed';
|
|
1885
1926
|
});
|
|
1927
|
+
console.log(`[Groove:Phase2] _executePhase2Check(${completedAgentId}): not all done, still waiting on [${still.join(', ')}]`);
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
console.log(`[Groove:Phase2] All phase 1 agents done for group [${group.waitFor.join(', ')}], spawning phase 2`);
|
|
1932
|
+
|
|
1933
|
+
// Mark group as in-flight (don't splice yet — wait for spawns to succeed)
|
|
1934
|
+
group._spawning = true;
|
|
1886
1935
|
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1936
|
+
const journalist = this.daemon.journalist;
|
|
1937
|
+
const phase1Idle = group.waitFor.every((id) => {
|
|
1938
|
+
const a = registry.get(id);
|
|
1939
|
+
if (!a) return true;
|
|
1940
|
+
const files = journalist?.getAgentFiles(a) || [];
|
|
1941
|
+
return files.length === 0;
|
|
1942
|
+
});
|
|
1943
|
+
|
|
1944
|
+
const groupTeamId = group.agents[0]?.teamId || this.daemon.teams.getDefault()?.id || null;
|
|
1945
|
+
if (!this._phase2Spawning) this._phase2Spawning = new Map();
|
|
1946
|
+
const spawnPromises = [];
|
|
1947
|
+
let reusedCount = 0;
|
|
1948
|
+
|
|
1949
|
+
for (const config of group.agents) {
|
|
1950
|
+
if (phase1Idle) config.prompt = '';
|
|
1951
|
+
|
|
1952
|
+
const teamId = config.teamId || this.daemon.teams.getDefault()?.id || null;
|
|
1953
|
+
const existing = teamId ? registry.getAll().find((a) =>
|
|
1954
|
+
a.teamId === teamId && a.role === config.role && a.id !== undefined
|
|
1955
|
+
) : null;
|
|
1956
|
+
|
|
1957
|
+
if (existing && (existing.status === 'running' || existing.status === 'starting')) {
|
|
1958
|
+
if (config.prompt) {
|
|
1959
|
+
this.sendMessage(existing.id, config.prompt, 'planner').catch((err) => {
|
|
1960
|
+
console.error(`[Groove:Phase2] Reuse message failed for ${existing.name}: ${err.message}`);
|
|
1961
|
+
});
|
|
1962
|
+
}
|
|
1963
|
+
this.daemon.audit.log('phase2.reuse', { id: existing.id, name: existing.name, role: existing.role });
|
|
1964
|
+
this.daemon.broadcast({
|
|
1965
|
+
type: 'phase2:spawned',
|
|
1966
|
+
agentId: existing.id,
|
|
1967
|
+
name: existing.name,
|
|
1968
|
+
role: existing.role,
|
|
1969
|
+
reused: true,
|
|
1899
1970
|
});
|
|
1971
|
+
reusedCount++;
|
|
1972
|
+
console.log(`[Groove:Phase2] Reused existing agent ${existing.name} (${existing.id}) for role ${config.role}`);
|
|
1973
|
+
continue;
|
|
1974
|
+
}
|
|
1900
1975
|
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
console.error(`[Groove] Phase 2 reuse message failed for ${existing.name}: ${err.message}`);
|
|
1923
|
-
});
|
|
1924
|
-
}
|
|
1925
|
-
this.daemon.audit.log('phase2.reuse', { id: existing.id, name: existing.name, role: existing.role });
|
|
1976
|
+
if (existing && (existing.status === 'completed' || existing.status === 'stopped' || existing.status === 'crashed' || existing.status === 'killed')) {
|
|
1977
|
+
config.name = existing.name;
|
|
1978
|
+
registry.remove(existing.id);
|
|
1979
|
+
this.daemon.locks.release(existing.id);
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
try {
|
|
1983
|
+
const validated = validateAgentConfig(config);
|
|
1984
|
+
if (!validated.teamId) validated.teamId = this.daemon.teams.getDefault()?.id || null;
|
|
1985
|
+
validated.metadata = { ...(validated.metadata || {}), isQcPhase2: true };
|
|
1986
|
+
const existingId = existing?.id || null;
|
|
1987
|
+
const p = this.spawn(validated).then((agent) => {
|
|
1988
|
+
registry.update(agent.id, { metadata: { ...(agent.metadata || {}), isQcPhase2: true } });
|
|
1989
|
+
this.daemon.broadcast({
|
|
1990
|
+
type: 'phase2:spawned',
|
|
1991
|
+
agentId: agent.id,
|
|
1992
|
+
oldAgentId: existingId,
|
|
1993
|
+
name: agent.name,
|
|
1994
|
+
role: agent.role,
|
|
1995
|
+
});
|
|
1996
|
+
if (existingId) {
|
|
1926
1997
|
this.daemon.broadcast({
|
|
1927
|
-
type: '
|
|
1928
|
-
agentId:
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1998
|
+
type: 'rotation:complete',
|
|
1999
|
+
agentId: agent.id,
|
|
2000
|
+
oldAgentId: existingId,
|
|
2001
|
+
agentName: agent.name,
|
|
2002
|
+
reason: 'phase2_respawn',
|
|
1932
2003
|
});
|
|
1933
|
-
continue;
|
|
1934
2004
|
}
|
|
2005
|
+
this.daemon.audit.log('phase2.autoSpawn', { id: agent.id, name: agent.name, role: agent.role });
|
|
2006
|
+
console.log(`[Groove:Phase2] Spawned ${agent.name} (${agent.id}) for role ${validated.role}`);
|
|
2007
|
+
return { status: 'spawned', agentId: agent.id };
|
|
2008
|
+
}).catch((err) => {
|
|
2009
|
+
console.error(`[Groove:Phase2] Spawn failed for ${config.role}: ${err.message}`);
|
|
2010
|
+
this.daemon.broadcast({
|
|
2011
|
+
type: 'phase2:failed',
|
|
2012
|
+
role: config.role,
|
|
2013
|
+
error: err.message,
|
|
2014
|
+
});
|
|
2015
|
+
throw err;
|
|
2016
|
+
});
|
|
2017
|
+
spawnPromises.push(p);
|
|
2018
|
+
} catch (err) {
|
|
2019
|
+
console.error(`[Groove:Phase2] Config invalid for ${config.role}: ${err.message}`);
|
|
2020
|
+
this.daemon.broadcast({
|
|
2021
|
+
type: 'phase2:failed',
|
|
2022
|
+
role: config.role,
|
|
2023
|
+
error: err.message,
|
|
2024
|
+
});
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
1935
2027
|
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
2028
|
+
if (spawnPromises.length === 0 && reusedCount === 0) {
|
|
2029
|
+
console.error(`[Groove:Phase2] No spawns attempted and no reuses for group [${group.waitFor.join(', ')}]`);
|
|
2030
|
+
this.daemon.broadcast({
|
|
2031
|
+
type: 'phase2:failed',
|
|
2032
|
+
role: group.agents.map(a => a.role).join(', '),
|
|
2033
|
+
error: 'No phase 2 agents could be spawned or reused',
|
|
2034
|
+
});
|
|
2035
|
+
// Remove failed group from pending and persist
|
|
2036
|
+
const idx = pending.indexOf(group);
|
|
2037
|
+
if (idx !== -1) pending.splice(idx, 1);
|
|
2038
|
+
this._persistPendingPhase2();
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
1942
2041
|
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
validated.metadata = { ...(validated.metadata || {}), isQcPhase2: true };
|
|
1947
|
-
const existingId = existing?.id || null;
|
|
1948
|
-
const p = this.spawn(validated).then((agent) => {
|
|
1949
|
-
registry.update(agent.id, { metadata: { ...(agent.metadata || {}), isQcPhase2: true } });
|
|
1950
|
-
this.daemon.broadcast({
|
|
1951
|
-
type: 'phase2:spawned',
|
|
1952
|
-
agentId: agent.id,
|
|
1953
|
-
oldAgentId: existingId,
|
|
1954
|
-
name: agent.name,
|
|
1955
|
-
role: agent.role,
|
|
1956
|
-
});
|
|
1957
|
-
if (existingId) {
|
|
1958
|
-
this.daemon.broadcast({
|
|
1959
|
-
type: 'rotation:complete',
|
|
1960
|
-
agentId: agent.id,
|
|
1961
|
-
oldAgentId: existingId,
|
|
1962
|
-
agentName: agent.name,
|
|
1963
|
-
reason: 'phase2_respawn',
|
|
1964
|
-
});
|
|
1965
|
-
}
|
|
1966
|
-
this.daemon.audit.log('phase2.autoSpawn', { id: agent.id, name: agent.name, role: agent.role });
|
|
1967
|
-
}).catch((err) => {
|
|
1968
|
-
console.error(`[Groove] Phase 2 spawn failed for ${config.role}: ${err.message}`);
|
|
1969
|
-
this.daemon.broadcast({
|
|
1970
|
-
type: 'phase2:failed',
|
|
1971
|
-
role: config.role,
|
|
1972
|
-
error: err.message,
|
|
1973
|
-
});
|
|
1974
|
-
});
|
|
1975
|
-
spawnPromises.push(p);
|
|
1976
|
-
} catch (err) {
|
|
1977
|
-
console.error(`[Groove] Phase 2 config invalid for ${config.role}: ${err.message}`);
|
|
1978
|
-
this.daemon.broadcast({
|
|
1979
|
-
type: 'phase2:failed',
|
|
1980
|
-
role: config.role,
|
|
1981
|
-
error: err.message,
|
|
1982
|
-
});
|
|
1983
|
-
}
|
|
1984
|
-
}
|
|
2042
|
+
if (spawnPromises.length > 0 && groupTeamId) {
|
|
2043
|
+
this._phase2Spawning.set(groupTeamId, (this._phase2Spawning.get(groupTeamId) || 0) + spawnPromises.length);
|
|
2044
|
+
}
|
|
1985
2045
|
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
2046
|
+
if (spawnPromises.length > 0) {
|
|
2047
|
+
Promise.allSettled(spawnPromises).then((results) => {
|
|
2048
|
+
const succeeded = results.filter(r => r.status === 'fulfilled').length;
|
|
2049
|
+
const failed = results.filter(r => r.status === 'rejected').length;
|
|
2050
|
+
|
|
2051
|
+
console.log(`[Groove:Phase2] Spawn results: ${succeeded} succeeded, ${failed} failed, ${reusedCount} reused`);
|
|
2052
|
+
|
|
2053
|
+
if (succeeded > 0 || reusedCount > 0) {
|
|
2054
|
+
// At least one spawn succeeded — remove group from pending
|
|
2055
|
+
const idx = pending.indexOf(group);
|
|
2056
|
+
if (idx !== -1) pending.splice(idx, 1);
|
|
2057
|
+
this._persistPendingPhase2();
|
|
2058
|
+
console.log(`[Groove:Phase2] Group removed from pending queue`);
|
|
2059
|
+
} else {
|
|
2060
|
+
// All spawns failed — keep group in pending for retry, broadcast failure
|
|
2061
|
+
group._spawning = false;
|
|
2062
|
+
console.error(`[Groove:Phase2] All spawns failed for group [${group.waitFor.join(', ')}], keeping in queue for retry`);
|
|
2063
|
+
this.daemon.broadcast({
|
|
2064
|
+
type: 'phase2:failed',
|
|
2065
|
+
role: group.agents.map(a => a.role).join(', '),
|
|
2066
|
+
error: 'All phase 2 spawn attempts failed',
|
|
2067
|
+
retryable: true,
|
|
1994
2068
|
});
|
|
1995
2069
|
}
|
|
2070
|
+
|
|
2071
|
+
// Clear in-flight counter
|
|
2072
|
+
if (groupTeamId) {
|
|
2073
|
+
const remaining = (this._phase2Spawning.get(groupTeamId) || 0) - spawnPromises.length;
|
|
2074
|
+
if (remaining <= 0) this._phase2Spawning.delete(groupTeamId);
|
|
2075
|
+
else this._phase2Spawning.set(groupTeamId, remaining);
|
|
2076
|
+
}
|
|
2077
|
+
});
|
|
2078
|
+
} else {
|
|
2079
|
+
// All agents were reused, no async spawns — remove group now
|
|
2080
|
+
const idx = pending.indexOf(group);
|
|
2081
|
+
if (idx !== -1) pending.splice(idx, 1);
|
|
2082
|
+
this._persistPendingPhase2();
|
|
2083
|
+
console.log(`[Groove:Phase2] Group removed from pending queue (all reused)`);
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
_persistPendingPhase2() {
|
|
2088
|
+
try {
|
|
2089
|
+
const pending = this.daemon._pendingPhase2 || [];
|
|
2090
|
+
const saveable = pending.filter(g => !g._spawning);
|
|
2091
|
+
const filePath = resolve(this.daemon.grooveDir, 'pending-phase2.json');
|
|
2092
|
+
if (saveable.length === 0) {
|
|
2093
|
+
try { unlinkSync(filePath); } catch { /* already gone */ }
|
|
2094
|
+
} else {
|
|
2095
|
+
writeFileSync(filePath, JSON.stringify(saveable, null, 2));
|
|
2096
|
+
}
|
|
2097
|
+
} catch (err) {
|
|
2098
|
+
console.error(`[Groove:Phase2] Failed to persist pending-phase2.json: ${err.message}`);
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
loadPendingPhase2() {
|
|
2103
|
+
try {
|
|
2104
|
+
const filePath = resolve(this.daemon.grooveDir, 'pending-phase2.json');
|
|
2105
|
+
if (!existsSync(filePath)) return;
|
|
2106
|
+
const data = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
2107
|
+
if (Array.isArray(data) && data.length > 0) {
|
|
2108
|
+
this.daemon._pendingPhase2 = this.daemon._pendingPhase2 || [];
|
|
2109
|
+
for (const group of data) {
|
|
2110
|
+
const isDupe = this.daemon._pendingPhase2.some(g =>
|
|
2111
|
+
g.waitFor.join(',') === group.waitFor.join(',')
|
|
2112
|
+
);
|
|
2113
|
+
if (!isDupe) this.daemon._pendingPhase2.push(group);
|
|
2114
|
+
}
|
|
2115
|
+
console.log(`[Groove:Phase2] Loaded ${data.length} pending phase 2 group(s) from disk`);
|
|
1996
2116
|
}
|
|
2117
|
+
} catch (err) {
|
|
2118
|
+
console.error(`[Groove:Phase2] Failed to load pending-phase2.json: ${err.message}`);
|
|
1997
2119
|
}
|
|
1998
2120
|
}
|
|
1999
2121
|
|
|
@@ -418,6 +418,7 @@ export function registerTeamRoutes(app, daemon) {
|
|
|
418
418
|
teamId: defaultTeamId,
|
|
419
419
|
})),
|
|
420
420
|
});
|
|
421
|
+
daemon.processes._persistPendingPhase2();
|
|
421
422
|
}
|
|
422
423
|
}
|
|
423
424
|
|
|
@@ -632,6 +633,7 @@ export function registerTeamRoutes(app, daemon) {
|
|
|
632
633
|
teamId: defaultTeamId,
|
|
633
634
|
})),
|
|
634
635
|
});
|
|
636
|
+
daemon.processes._persistPendingPhase2();
|
|
635
637
|
}
|
|
636
638
|
|
|
637
639
|
daemon.audit.log('team-builder.launch', {
|
|
@@ -453,7 +453,7 @@ export class TunnelManager {
|
|
|
453
453
|
try {
|
|
454
454
|
this.daemon.broadcast({ type: 'tunnel.status', data: { id, step: 'checking' } });
|
|
455
455
|
|
|
456
|
-
// Get remote daemon version
|
|
456
|
+
// Get remote daemon version through the already-open tunnel
|
|
457
457
|
const resp = await fetch(`http://localhost:${localPort}/api/status`, {
|
|
458
458
|
signal: AbortSignal.timeout(5000),
|
|
459
459
|
});
|
|
@@ -462,15 +462,11 @@ export class TunnelManager {
|
|
|
462
462
|
const remoteVer = status.version;
|
|
463
463
|
if (!remoteVer) return;
|
|
464
464
|
|
|
465
|
-
// Check latest version on npm (
|
|
466
|
-
const target = `${config.user}@${config.host}`;
|
|
467
|
-
const keyArgs = config.sshKeyPath ? ['-i', config.sshKeyPath] : [];
|
|
468
|
-
const sshBase = [...keyArgs, '-p', String(config.port || 22), '-o', 'ConnectTimeout=10', '-o', 'BatchMode=yes', target];
|
|
469
|
-
|
|
465
|
+
// Check latest version on npm locally (same registry everywhere, no extra SSH)
|
|
470
466
|
let npmVer;
|
|
471
467
|
try {
|
|
472
|
-
npmVer = execFileSync('
|
|
473
|
-
encoding: 'utf8', timeout:
|
|
468
|
+
npmVer = execFileSync('npm', ['view', 'groove-dev', 'version'], {
|
|
469
|
+
encoding: 'utf8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
474
470
|
}).trim();
|
|
475
471
|
} catch { return; }
|
|
476
472
|
|